/*
 *
 * 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.qpid.server.filter;
//
// Based on like named file from r450141 of the Apache ActiveMQ project <http://www.activemq.org/site/home.html>
//

import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;

import org.apache.qpid.server.queue.Filterable;

/**
 * A filter performing a comparison of two objects
 */
public abstract class ComparisonExpression extends BinaryExpression implements BooleanExpression
{

    public static BooleanExpression createBetween(Expression value, Expression left, Expression right)
    {
        return LogicExpression.createAND(createGreaterThanEqual(value, left), createLessThanEqual(value, right));
    }

    public static BooleanExpression createNotBetween(Expression value, Expression left, Expression right)
    {
        return LogicExpression.createOR(createLessThan(value, left), createGreaterThan(value, right));
    }

    private static final HashSet<Character> REGEXP_CONTROL_CHARS = new HashSet<Character>();

    static
    {
        REGEXP_CONTROL_CHARS.add('.');
        REGEXP_CONTROL_CHARS.add('\\');
        REGEXP_CONTROL_CHARS.add('[');
        REGEXP_CONTROL_CHARS.add(']');
        REGEXP_CONTROL_CHARS.add('^');
        REGEXP_CONTROL_CHARS.add('$');
        REGEXP_CONTROL_CHARS.add('?');
        REGEXP_CONTROL_CHARS.add('*');
        REGEXP_CONTROL_CHARS.add('+');
        REGEXP_CONTROL_CHARS.add('{');
        REGEXP_CONTROL_CHARS.add('}');
        REGEXP_CONTROL_CHARS.add('|');
        REGEXP_CONTROL_CHARS.add('(');
        REGEXP_CONTROL_CHARS.add(')');
        REGEXP_CONTROL_CHARS.add(':');
        REGEXP_CONTROL_CHARS.add('&');
        REGEXP_CONTROL_CHARS.add('<');
        REGEXP_CONTROL_CHARS.add('>');
        REGEXP_CONTROL_CHARS.add('=');
        REGEXP_CONTROL_CHARS.add('!');
    }

    static class LikeExpression extends UnaryExpression implements BooleanExpression
    {

        Pattern likePattern;

        /**
         * @param right
         */
        public LikeExpression(Expression right, String like, int escape)
        {
            super(right);

            StringBuffer regexp = new StringBuffer(like.length() * 2);
            regexp.append("\\A"); // The beginning of the input
            for (int i = 0; i < like.length(); i++)
            {
                char c = like.charAt(i);
                if (escape == (0xFFFF & c))
                {
                    i++;
                    if (i >= like.length())
                    {
                        // nothing left to escape...
                        break;
                    }

                    char t = like.charAt(i);
                    regexp.append("\\x");
                    regexp.append(Integer.toHexString(0xFFFF & t));
                }
                else if (c == '%')
                {
                    regexp.append(".*?"); // Do a non-greedy match
                }
                else if (c == '_')
                {
                    regexp.append("."); // match one
                }
                else if (REGEXP_CONTROL_CHARS.contains(c))
                {
                    regexp.append("\\x");
                    regexp.append(Integer.toHexString(0xFFFF & c));
                }
                else
                {
                    regexp.append(c);
                }
            }

            regexp.append("\\z"); // The end of the input

            likePattern = Pattern.compile(regexp.toString(), Pattern.DOTALL);
        }

        /**
         *  org.apache.activemq.filter.UnaryExpression#getExpressionSymbol()
         */
        public String getExpressionSymbol()
        {
            return "LIKE";
        }

        /**
         *  org.apache.activemq.filter.Expression#evaluate(MessageEvaluationContext)
         */
        public Object evaluate(Filterable message)
        {

            Object rv = this.getRight().evaluate(message);

            if (rv == null)
            {
                return null;
            }

            if (!(rv instanceof String))
            {
                return
                    Boolean.FALSE;
                    // throw new RuntimeException("LIKE can only operate on String identifiers.  LIKE attemped on: '" + rv.getClass());
            }

            return likePattern.matcher((String) rv).matches() ? Boolean.TRUE : Boolean.FALSE;
        }

        public boolean matches(Filterable message)
        {
            Object object = evaluate(message);

            return (object != null) && (object == Boolean.TRUE);
        }
    }

    public static BooleanExpression createLike(Expression left, String right, String escape)
    {
        if ((escape != null) && (escape.length() != 1))
        {
            throw new RuntimeException(
                "The ESCAPE string litteral is invalid.  It can only be one character.  Litteral used: " + escape);
        }

        int c = -1;
        if (escape != null)
        {
            c = 0xFFFF & escape.charAt(0);
        }

        return new LikeExpression(left, right, c);
    }

    public static BooleanExpression createNotLike(Expression left, String right, String escape)
    {
        return UnaryExpression.createNOT(createLike(left, right, escape));
    }

    public static BooleanExpression createInFilter(Expression left, List elements)
    {

        if (!(left instanceof PropertyExpression))
        {
            throw new RuntimeException("Expected a property for In expression, got: " + left);
        }

        return UnaryExpression.createInExpression((PropertyExpression) left, elements, false);

    }

    public static BooleanExpression createNotInFilter(Expression left, List elements)
    {

        if (!(left instanceof PropertyExpression))
        {
            throw new RuntimeException("Expected a property for In expression, got: " + left);
        }

        return UnaryExpression.createInExpression((PropertyExpression) left, elements, true);

    }

    public static BooleanExpression createIsNull(Expression left)
    {
        return doCreateEqual(left, ConstantExpression.NULL);
    }

    public static BooleanExpression createIsNotNull(Expression left)
    {
        return UnaryExpression.createNOT(doCreateEqual(left, ConstantExpression.NULL));
    }

    public static BooleanExpression createNotEqual(Expression left, Expression right)
    {
        return UnaryExpression.createNOT(createEqual(left, right));
    }

    public static BooleanExpression createEqual(Expression left, Expression right)
    {
        checkEqualOperand(left);
        checkEqualOperand(right);
        checkEqualOperandCompatability(left, right);

        return doCreateEqual(left, right);
    }

    private static BooleanExpression doCreateEqual(Expression left, Expression right)
    {
        return new EqualExpression(left, right);
    }

    public static BooleanExpression createGreaterThan(final Expression left, final Expression right)
    {
        checkLessThanOperand(left);
        checkLessThanOperand(right);

        return new ComparisonExpression(left, right)
            {
                protected boolean asBoolean(int answer)
                {
                    return answer > 0;
                }

                public String getExpressionSymbol()
                {
                    return ">";
                }
            };
    }

    public static BooleanExpression createGreaterThanEqual(final Expression left, final Expression right)
    {
        checkLessThanOperand(left);
        checkLessThanOperand(right);

        return new ComparisonExpression(left, right)
            {
                protected boolean asBoolean(int answer)
                {
                    return answer >= 0;
                }

                public String getExpressionSymbol()
                {
                    return ">=";
                }
            };
    }

    public static BooleanExpression createLessThan(final Expression left, final Expression right)
    {
        checkLessThanOperand(left);
        checkLessThanOperand(right);

        return new ComparisonExpression(left, right)
            {

                protected boolean asBoolean(int answer)
                {
                    return answer < 0;
                }

                public String getExpressionSymbol()
                {
                    return "<";
                }

            };
    }

    public static BooleanExpression createLessThanEqual(final Expression left, final Expression right)
    {
        checkLessThanOperand(left);
        checkLessThanOperand(right);

        return new ComparisonExpression(left, right)
            {

                protected boolean asBoolean(int answer)
                {
                    return answer <= 0;
                }

                public String getExpressionSymbol()
                {
                    return "<=";
                }
            };
    }

    /**
     * Only Numeric expressions can be used in >, >=, < or <= expressions.s
     *
     * @param expr
     */
    public static void checkLessThanOperand(Expression expr)
    {
        if (expr instanceof ConstantExpression)
        {
            Object value = ((ConstantExpression) expr).getValue();
            if (value instanceof Number)
            {
                return;
            }

            // Else it's boolean or a String..
            throw new RuntimeException("Value '" + expr + "' cannot be compared.");
        }

        if (expr instanceof BooleanExpression)
        {
            throw new RuntimeException("Value '" + expr + "' cannot be compared.");
        }
    }

    /**
     * Validates that the expression can be used in == or <> expression.
     * Cannot not be NULL TRUE or FALSE litterals.
     *
     * @param expr
     */
    public static void checkEqualOperand(Expression expr)
    {
        if (expr instanceof ConstantExpression)
        {
            Object value = ((ConstantExpression) expr).getValue();
            if (value == null)
            {
                throw new RuntimeException("'" + expr + "' cannot be compared.");
            }
        }
    }

    /**
     *
     * @param left
     * @param right
     */
    private static void checkEqualOperandCompatability(Expression left, Expression right)
    {
        if ((left instanceof ConstantExpression) && (right instanceof ConstantExpression))
        {
            if ((left instanceof BooleanExpression) && !(right instanceof BooleanExpression))
            {
                throw new RuntimeException("'" + left + "' cannot be compared with '" + right + "'");
            }
        }
    }

    /**
     * @param left
     * @param right
     */
    public ComparisonExpression(Expression left, Expression right)
    {
        super(left, right);
    }

    public Object evaluate(Filterable message)
    {
        Comparable lv = (Comparable) left.evaluate(message);
        if (lv == null)
        {
            return null;
        }

        Comparable rv = (Comparable) right.evaluate(message);
        if (rv == null)
        {
            return null;
        }

        return compare(lv, rv);
    }

    protected Boolean compare(Comparable lv, Comparable rv)
    {
        Class lc = lv.getClass();
        Class rc = rv.getClass();
        // If the the objects are not of the same type,
        // try to convert up to allow the comparison.
        if (lc != rc)
        {
            if (lc == Byte.class)
            {
                if (rc == Short.class)
                {
                    lv = ((Number) lv).shortValue();
                }
                else if (rc == Integer.class)
                {
                    lv = ((Number) lv).intValue();
                }
                else if (rc == Long.class)
                {
                    lv = ((Number) lv).longValue();
                }
                else if (rc == Float.class)
                {
                    lv = ((Number) lv).floatValue();
                }
                else if (rc == Double.class)
                {
                    lv = ((Number) lv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else if (lc == Short.class)
            {
                if (rc == Integer.class)
                {
                    lv = ((Number) lv).intValue();
                }
                else if (rc == Long.class)
                {
                    lv = ((Number) lv).longValue();
                }
                else if (rc == Float.class)
                {
                    lv = ((Number) lv).floatValue();
                }
                else if (rc == Double.class)
                {
                    lv = ((Number) lv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else if (lc == Integer.class)
            {
                if (rc == Long.class)
                {
                    lv = ((Number) lv).longValue();
                }
                else if (rc == Float.class)
                {
                    lv = ((Number) lv).floatValue();
                }
                else if (rc == Double.class)
                {
                    lv = ((Number) lv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else if (lc == Long.class)
            {
                if (rc == Integer.class)
                {
                    rv = ((Number) rv).longValue();
                }
                else if (rc == Float.class)
                {
                    lv = ((Number) lv).floatValue();
                }
                else if (rc == Double.class)
                {
                    lv = ((Number) lv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else if (lc == Float.class)
            {
                if (rc == Integer.class)
                {
                    rv = ((Number) rv).floatValue();
                }
                else if (rc == Long.class)
                {
                    rv = ((Number) rv).floatValue();
                }
                else if (rc == Double.class)
                {
                    lv = ((Number) lv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else if (lc == Double.class)
            {
                if (rc == Integer.class)
                {
                    rv = ((Number) rv).doubleValue();
                }
                else if (rc == Long.class)
                {
                    rv = ((Number) rv).doubleValue();
                }
                else if (rc == Float.class)
                {
                    rv = ((Number) rv).doubleValue();
                }
                else
                {
                    return Boolean.FALSE;
                }
            }
            else
            {
                return Boolean.FALSE;
            }
        }

        return asBoolean(lv.compareTo(rv)) ? Boolean.TRUE : Boolean.FALSE;
    }

    protected abstract boolean asBoolean(int answer);

    public boolean matches(Filterable message)
    {
        Object object = evaluate(message);

        return (object != null) && (object == Boolean.TRUE);
    }

    private static class EqualExpression extends ComparisonExpression
    {
        public EqualExpression(final Expression left, final Expression right)
        {
            super(left, right);
        }

        public Object evaluate(Filterable message)
        {
            Object lv = left.evaluate(message);
            Object rv = right.evaluate(message);

            // Iff one of the values is null
            if ((lv == null) ^ (rv == null))
            {
                return Boolean.FALSE;
            }

            if ((lv == rv) || lv.equals(rv))
            {
                return Boolean.TRUE;
            }

            if ((lv instanceof Comparable) && (rv instanceof Comparable))
            {
                return compare((Comparable) lv, (Comparable) rv);
            }

            return Boolean.FALSE;
        }

        protected boolean asBoolean(int answer)
        {
            return answer == 0;
        }

        public String getExpressionSymbol()
        {
            return "=";
        }
    }
}
