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

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

/**
 * An expression which performs an operation on two expression values
 */
public abstract class UnaryExpression implements Expression
{

    private static final BigDecimal BD_LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE);
    private Expression right;

    public static Expression createNegate(Expression left)
    {
        return new NegativeExpression(left);
    }

    public static BooleanExpression createInExpression(PropertyExpression right, List elements, final boolean not)
    {

        // Use a HashSet if there are many elements.
        Collection t;
        if (elements.size() == 0)
        {
            t = null;
        }
        else if (elements.size() < 5)
        {
            t = elements;
        }
        else
        {
            t = new HashSet(elements);
        }

        final Collection inList = t;

        return new InExpression(right, inList, not);
    }

    abstract static class BooleanUnaryExpression extends UnaryExpression implements BooleanExpression
    {
        public BooleanUnaryExpression(Expression left)
        {
            super(left);
        }

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

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

    public static BooleanExpression createNOT(BooleanExpression left)
    {
        return new NotExpression(left);
    }

    public static BooleanExpression createBooleanCast(Expression left)
    {
        return new BooleanCastExpression(left);
    }

    private static Number negate(Number left)
    {
        Class clazz = left.getClass();
        if (clazz == Integer.class)
        {
            return -left.intValue();
        }
        else if (clazz == Long.class)
        {
            return -left.longValue();
        }
        else if (clazz == Float.class)
        {
            return -left.floatValue();
        }
        else if (clazz == Double.class)
        {
            return -left.doubleValue();
        }
        else if (clazz == BigDecimal.class)
        {
            // We ussually get a big deciamal when we have Long.MIN_VALUE constant in the
            // Selector.  Long.MIN_VALUE is too big to store in a Long as a positive so we store it
            // as a Big decimal.  But it gets Negated right away.. to here we try to covert it back
            // to a Long.
            BigDecimal bd = (BigDecimal) left;
            bd = bd.negate();

            if (BD_LONG_MIN_VALUE.compareTo(bd) == 0)
            {
                return Long.MIN_VALUE;
            }

            return bd;
        }
        else
        {
            throw new SelectorParsingException("Don't know how to negate: " + left);
        }
    }

    public UnaryExpression(Expression left)
    {
        this.right = left;
    }

    public Expression getRight()
    {
        return right;
    }

    /**
     * @see java.lang.Object#toString()
     */
    public String toString()
    {
        return "(" + getExpressionSymbol() + " " + right.toString() + ")";
    }

    /**
     * TODO: more efficient hashCode()
     *
     * @see java.lang.Object#hashCode()
     */
    public int hashCode()
    {
        return toString().hashCode();
    }

    /**
     * TODO: more efficient hashCode()
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object o)
    {
        return ((o != null) && this.getClass().equals(o.getClass())) && toString().equals(o.toString());
    }

    /**
     * Returns the symbol that represents this binary expression.  For example, addition is
     * represented by "+"
     *
     * @return symbol
     */
    public abstract String getExpressionSymbol();

    private static class NegativeExpression extends UnaryExpression
    {
        public NegativeExpression(final Expression left)
        {
            super(left);
        }

        public Object evaluate(FilterableMessage message)
        {
            Object rvalue = getRight().evaluate(message);
            if (rvalue == null)
            {
                return null;
            }

            if (rvalue instanceof Number)
            {
                return negate((Number) rvalue);
            }

            return null;
        }

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

    private static class InExpression extends BooleanUnaryExpression
    {
        private final Collection _inList;
        private final boolean _not;

        public InExpression(final PropertyExpression right, final Collection inList, final boolean not)
        {
            super(right);
            _inList = inList;
            _not = not;
        }

        public Object evaluate(FilterableMessage message)
        {

            Object rvalue = getRight().evaluate(message);
            if (rvalue == null)
            {
                return null;
            }

            if (rvalue.getClass() != String.class)
            {
                return null;
            }

            if (((_inList != null) && _inList.contains(rvalue)) ^ _not)
            {
                return Boolean.TRUE;
            }
            else
            {
                return Boolean.FALSE;
            }

        }

        public String toString()
        {
            StringBuilder answer = new StringBuilder(String.valueOf(getRight()));
            answer.append(" ");
            answer.append(getExpressionSymbol());
            answer.append(" ( ");

            int count = 0;
            for (Object o : _inList)
            {
                if (count != 0)
                {
                    answer.append(", ");
                }

                answer.append(o);
                count++;
            }

            answer.append(" )");

            return answer.toString();
        }

        public String getExpressionSymbol()
        {
            if (_not)
            {
                return "NOT IN";
            }
            else
            {
                return "IN";
            }
        }
    }

    private static class NotExpression extends BooleanUnaryExpression
    {
        public NotExpression(final BooleanExpression left)
        {
            super(left);
        }

        public Object evaluate(FilterableMessage message)
        {
            Boolean lvalue = (Boolean) getRight().evaluate(message);
            if (lvalue == null)
            {
                return null;
            }

            return lvalue ? Boolean.FALSE : Boolean.TRUE;
        }

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

    private static class BooleanCastExpression extends BooleanUnaryExpression
    {
        public BooleanCastExpression(final Expression left)
        {
            super(left);
        }

        public Object evaluate(FilterableMessage message)
        {
            Object rvalue = getRight().evaluate(message);
            if (rvalue == null)
            {
                return null;
            }

            if (!rvalue.getClass().equals(Boolean.class))
            {
                return Boolean.FALSE;
            }

            return ((Boolean) rvalue) ? Boolean.TRUE : Boolean.FALSE;
        }

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

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