/*
 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.javafx.fxml.expression;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.sun.javafx.fxml.BeanAdapter;
import static com.sun.javafx.fxml.expression.Operator.*;

/**
 * Abstract base class for expressions. Also provides static methods for
 * creating arithmetic and logical expressions as well as accessing namespace
 * values by key path.
 */
public abstract class Expression<T> {
    // Expression parser class
    private static class Parser {
        public static class Token {
            public Token(TokenType type, Object value) {
                this.type = type;
                this.value = value;
            }

            public final TokenType type;
            public final Object value;

            @Override
            public String toString() {
                return value.toString();
            }
        }

        public enum TokenType {
            LITERAL,
            VARIABLE,
            FUNCTION,
            UNARY_OPERATOR,
            BINARY_OPERATOR,
            BEGIN_GROUP,
            END_GROUP
        }

        private int c = -1;
        private char[] pushbackBuffer = new char[PUSHBACK_BUFFER_SIZE];

        private static final int PUSHBACK_BUFFER_SIZE = 6;

        public Expression parse(Reader reader) throws IOException {
            LinkedList<Token> tokens = tokenize(new PushbackReader(reader, PUSHBACK_BUFFER_SIZE));

            LinkedList<Expression> stack = new LinkedList<Expression>();

            for (Token token : tokens) {
                Expression<?> expression;
                switch (token.type) {
                    case LITERAL: {
                        expression = new LiteralExpression(token.value);
                        break;
                    }

                    case VARIABLE: {
                        expression = new VariableExpression((KeyPath)token.value);
                        break;
                    }

                    case FUNCTION: {
                        // TODO Create a new FunctionExpression type; this
                        // class will have a property of type Method that
                        // refers to a method defined by the "scope context"
                        // (e.g. an FXML document controller), which must be
                        // set prior to evaluating the expression; it will
                        // also have a list of argument Expressions
                        expression = null;
                        break;
                    }

                    case UNARY_OPERATOR: {
                        Operator operator = (Operator)token.value;
                        Expression operand = stack.pop();

                        switch(operator) {
                            case NEGATE:
                                expression = negate(operand);
                                break;
                            case NOT:
                                expression = not(operand);
                                break;
                            default:
                                throw new UnsupportedOperationException();

                        }

                        break;
                    }

                    case BINARY_OPERATOR: {
                        Operator operator = (Operator)token.value;
                        Expression right = stack.pop();
                        Expression left = stack.pop();

                        switch(operator) {
                            case ADD:
                                expression = add(left, right);
                                break;
                            case SUBTRACT:
                                expression = subtract(left, right);
                                break;
                            case MULTIPLY:
                                expression = multiply(left, right);
                                break;
                            case DIVIDE:
                                expression = divide(left, right);
                                break;
                            case MODULO:
                                expression = modulo(left, right);
                                break;
                            case GREATER_THAN:
                                expression = greaterThan(left, right);
                                break;
                            case GREATER_THAN_OR_EQUAL_TO:
                                expression = greaterThanOrEqualTo(left, right);
                                break;
                            case LESS_THAN:
                                expression = lessThan(left, right);
                                break;
                            case LESS_THAN_OR_EQUAL_TO:
                                expression = lessThanOrEqualTo(left, right);
                                break;
                            case EQUAL_TO:
                                expression = equalTo(left, right);
                                break;
                            case NOT_EQUAL_TO:
                                expression = notEqualTo(left, right);
                                break;
                            case AND:
                                expression = and(left, right);
                                break;
                            case OR:
                                expression = or(left, right);
                                break;
                            default:
                                throw new UnsupportedOperationException();
                        }

                        break;
                    }

                    default: {
                        throw new UnsupportedOperationException();
                    }
                }

                stack.push(expression);
            }

            if (stack.size() != 1) {
                throw new IllegalArgumentException("Invalid expression.");
            }

            return stack.peek();
        }

        private LinkedList<Token> tokenize(PushbackReader reader) throws IOException {
            // Read the string into a postfix list of tokens
            LinkedList<Token> tokens = new LinkedList<Token>();
            LinkedList<Token> stack = new LinkedList<Token>();

            c = reader.read();
            boolean unary = true;

            while (c != -1) {
                // Skip whitespace
                while (c != -1 && Character.isWhitespace(c)) {
                    c = reader.read();
                }

                if (c != -1) {
                    Token token;

                    if (c == 'n') {
                        if (readKeyword(reader, NULL_KEYWORD)) {
                            token = new Token(TokenType.LITERAL, null);
                        } else {
                            token = new Token(TokenType.VARIABLE, KeyPath.parse(reader));
                            c = reader.read();
                        }
                    } else if (c == '"' || c == '\'') {
                        StringBuilder stringBuilder = new StringBuilder();

                        // Use the same delimiter to close the string
                        int t = c;

                        // Move to the next character after the delimiter
                        c = reader.read();

                        while (c != -1 && c != t) {
                            if (!Character.isISOControl(c)) {
                                if (c == '\\') {
                                    c = reader.read();

                                    if (c == 'b') {
                                        c = '\b';
                                    } else if (c == 'f') {
                                        c = '\f';
                                    } else if (c == 'n') {
                                        c = '\n';
                                    } else if (c == 'r') {
                                        c = '\r';
                                    } else if (c == 't') {
                                        c = '\t';
                                    } else if (c == 'u') {
                                        StringBuilder unicodeValueBuilder = new StringBuilder();
                                        while (unicodeValueBuilder.length() < 4) {
                                            c = reader.read();
                                            unicodeValueBuilder.append((char)c);
                                        }

                                        String unicodeValue = unicodeValueBuilder.toString();
                                        c = (char)Integer.parseInt(unicodeValue, 16);
                                    } else {
                                        if (!(c == '\\'
                                            || c == '/'
                                            || c == '\"'
                                            || c == '\''
                                            || c == t)) {
                                            throw new IllegalArgumentException("Unsupported escape sequence.");
                                        }
                                    }
                                }

                                stringBuilder.append((char)c);
                            }

                            c = reader.read();
                        }

                        if (c != t) {
                            throw new IllegalArgumentException("Unterminated string.");
                        }

                        // Move to the next character after the delimiter
                        c = reader.read();

                        token = new Token(TokenType.LITERAL, stringBuilder.toString());
                    } else if (Character.isDigit(c)) {
                        StringBuilder numberBuilder = new StringBuilder();
                        boolean integer = true;

                        while (c != -1 && (Character.isDigit(c) || c == '.'
                            || c == 'e' || c == 'E')) {
                            numberBuilder.append((char)c);
                            integer &= !(c == '.');
                            c = reader.read();
                        }

                        Number value;
                        if (integer) {
                            value = Long.parseLong(numberBuilder.toString());
                        } else {
                            value = Double.parseDouble(numberBuilder.toString());
                        }

                        token = new Token(TokenType.LITERAL, value);
                    } else if (c == 't') {
                        if (readKeyword(reader, TRUE_KEYWORD)) {
                            token = new Token(TokenType.LITERAL, true);
                        } else {
                            token = new Token(TokenType.VARIABLE, KeyPath.parse(reader));
                            c = reader.read();
                        }
                    } else if (c == 'f') {
                        if (readKeyword(reader, FALSE_KEYWORD)) {
                            token = new Token(TokenType.LITERAL, false);
                        } else {
                            token = new Token(TokenType.VARIABLE, KeyPath.parse(reader));
                            c = reader.read();
                        }
                    } else if (Character.isJavaIdentifierStart(c)) {
                        reader.unread(c);

                        // TODO Here (and everywhere else where we call KeyPath.parse()),
                        // read the path value. If c == '(' when this method returns, the
                        // path refers to a function; read the arguments and create a
                        // FUNCTION token
                        token = new Token(TokenType.VARIABLE, KeyPath.parse(reader));
                        c = reader.read();
                    } else {
                        boolean readNext = true;
                        if (unary) {
                            switch(c) {
                                case '-':
                                    token = new Token(TokenType.UNARY_OPERATOR, NEGATE);
                                    break;
                                case '!':
                                    token = new Token(TokenType.UNARY_OPERATOR, NOT);
                                    break;
                                case '(':
                                    token = new Token(TokenType.BEGIN_GROUP, null);
                                    break;
                                default:
                                    throw new IllegalArgumentException("Unexpected character in expression.");
                            }
                        } else {
                            switch(c) {
                                case '+':
                                    token = new Token(TokenType.BINARY_OPERATOR, ADD);
                                    break;
                                case '-':
                                    token = new Token(TokenType.BINARY_OPERATOR, SUBTRACT);
                                    break;
                                case '*':
                                    token = new Token(TokenType.BINARY_OPERATOR, MULTIPLY);
                                    break;
                                case '/':
                                    token = new Token(TokenType.BINARY_OPERATOR, DIVIDE);
                                    break;
                                case '%':
                                    token = new Token(TokenType.BINARY_OPERATOR, MODULO);
                                    break;
                                case '=':
                                    c = reader.read();
                                    if (c == '=') {
                                        token = new Token(TokenType.BINARY_OPERATOR, EQUAL_TO);
                                    } else {
                                        throw new IllegalArgumentException("Unexpected character in expression.");
                                    }
                                    break;
                                case '!':
                                    c = reader.read();

                                    if (c == '=') {
                                        token = new Token(TokenType.BINARY_OPERATOR, NOT_EQUAL_TO);
                                    } else {
                                        throw new IllegalArgumentException("Unexpected character in expression.");
                                    }
                                    break;
                                case '>':
                                    c = reader.read();

                                    if (c == '=') {
                                        token = new Token(TokenType.BINARY_OPERATOR, GREATER_THAN_OR_EQUAL_TO);
                                    } else {
                                        readNext = false;
                                        token = new Token(TokenType.BINARY_OPERATOR, GREATER_THAN);
                                    }
                                    break;
                                case '<':
                                    c = reader.read();

                                    if (c == '=') {
                                        token = new Token(TokenType.BINARY_OPERATOR, LESS_THAN_OR_EQUAL_TO);
                                    } else {
                                        readNext = false;
                                        token = new Token(TokenType.BINARY_OPERATOR, LESS_THAN);
                                    }
                                    break;
                                case '&':
                                    c = reader.read();

                                    if (c == '&') {
                                        token = new Token(TokenType.BINARY_OPERATOR, AND);
                                    } else {
                                        throw new IllegalArgumentException("Unexpected character in expression.");
                                    }
                                    break;
                                case '|':
                                    c = reader.read();

                                    if (c == '|') {
                                        token = new Token(TokenType.BINARY_OPERATOR, OR);
                                    } else {
                                        throw new IllegalArgumentException("Unexpected character in expression.");
                                    }
                                    break;

                                case ')':
                                    token = new Token(TokenType.END_GROUP, null);
                                    break;
                                default:
                                    throw new IllegalArgumentException("Unexpected character in expression.");
                            }

                        }
                        if (readNext) {
                            c = reader.read();
                        }
                    }

                    // Process the token
                    switch (token.type) {
                        case LITERAL:
                        case VARIABLE: {
                            tokens.add(token);
                            break;
                        }

                        case UNARY_OPERATOR:
                        case BINARY_OPERATOR: {
                            int priority = ((Operator)token.value).getPriority();

                            while (!stack.isEmpty()
                                && stack.peek().type != TokenType.BEGIN_GROUP
                                && ((Operator)stack.peek().value).getPriority() >= priority
                                && ((Operator)stack.peek().value).getPriority() != Operator.MAX_PRIORITY) {
                                tokens.add(stack.pop());
                            }

                            stack.push(token);
                            break;
                        }

                        case BEGIN_GROUP: {
                            stack.push(token);
                            break;
                        }

                        case END_GROUP: {
                            for (Token t = stack.pop(); t.type != TokenType.BEGIN_GROUP; t = stack.pop()) {
                                tokens.add(t);
                            }

                            break;
                        }

                        default: {
                            throw new UnsupportedOperationException();
                        }
                    }

                    unary = !(token.type == TokenType.LITERAL || token.type == TokenType.VARIABLE || token.type == TokenType.END_GROUP);
                }
            }

            while (!stack.isEmpty()) {
                tokens.add(stack.pop());
            }

            return tokens;
        }

        private boolean readKeyword(PushbackReader reader, String keyword) throws IOException {
            int n = keyword.length();
            int i = 0;

            while (c != -1 && i < n) {
                pushbackBuffer[i] = (char)c;
                if (keyword.charAt(i) != c) {
                    break;
                }

                c = reader.read();
                i++;
            }

            boolean result;
            if (i < n) {
                reader.unread(pushbackBuffer, 0, i + 1);
                result = false;
            } else {
                result = true;
            }

            return result;
        }

    }

    private static final String NULL_KEYWORD = "null";
    private static final String TRUE_KEYWORD = "true";
    private static final String FALSE_KEYWORD = "false";

    /**
     * Evaluates the expression.
     *
     * @param namespace
     * The namespace against which the expression will be evaluated.
     *
     * @return
     * The result of evaluating the expression.
     */
    public abstract T evaluate(Object namespace);

    /**
     * Updates the expression value.
     *
     * @param namespace
     * The namespace against which the expression will be evaluated.
     *
     * @param value
     * The value to assign to the expression.
     */
    public abstract void update(Object namespace, T value);

    /**
     * Tests whether the expression is defined.
     *
     * @param namespace
     * The namespace against which the expression will be evaluated.
     *
     * @return
     * <tt>true</tt> if the expression is defined; <tt>false</tt>, otherwise.
     */
    public abstract boolean isDefined(Object namespace);

    /**
     * Tests whether the expression represents an l-value (i.e. can be
     * assigned to).
     *
     * @return
     * <tt>true</tt> if the expression is an l-value; <tt>false</tt>,
     * otherwise.
     */
    public abstract boolean isLValue();

    /**
     * Returns a list of arguments to this expression.
     */
    public List<KeyPath> getArguments() {
        ArrayList<KeyPath> arguments = new ArrayList<KeyPath>();
        getArguments(arguments);

        return arguments;
    }

    /**
     * Populates a list of arguments to this expression.
     */
    protected abstract void getArguments(List<KeyPath> arguments);

    /**
     * Returns the value at a given path within a namespace.
     *
     * @param namespace
     * @param keyPath
     *
     * @return
     * The value at the given path, or <tt>null</tt> if no such value exists.
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Object namespace, KeyPath keyPath) {
        if (keyPath == null) {
            throw new NullPointerException();
        }

        return (T)get(namespace, keyPath.iterator());
    }

    /**
     * Returns the value at a given path within a namespace.
     *
     * @param namespace
     * @param keyPathIterator
     *
     * @return
     * The value at the given path, or <tt>null</tt> if no such value exists.
     */
    @SuppressWarnings("unchecked")
    private static <T> T get(Object namespace, Iterator<String> keyPathIterator) {
        if (keyPathIterator == null) {
            throw new NullPointerException();
        }

        T value;
        if (keyPathIterator.hasNext()) {
            // TODO Remove cast to T when build is updated to Java 7
            value = (T)get(get(namespace, keyPathIterator.next()), keyPathIterator);
        } else {
            value = (T)namespace;
        }

        return value;
    }

    /**
     * Returns the value at a given key within a namespace.
     *
     * @param namespace
     * @param key
     *
     * @return
     * The value at the given key, or <tt>null</tt> if no such value exists.
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Object namespace, String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        Object value;
        if (namespace instanceof List<?>) {
            List<Object> list = (List<Object>)namespace;
            value = list.get(Integer.parseInt(key));
        } else if (namespace != null) {
            Map<String, Object> map;
            if (namespace instanceof Map<?, ?>) {
                map = (Map<String, Object>)namespace;
            } else {
                map = new BeanAdapter(namespace);
            }

            value = map.get(key);
        } else {
            value = null;
        }

        return (T)value;
    }

    /**
     * Sets the value at a given path within a namespace.
     *
     * @param namespace
     * @param keyPath
     * @param value
     */
    public static void set(Object namespace, KeyPath keyPath, Object value) {
        if (keyPath == null) {
            throw new NullPointerException();
        }

        set(namespace, keyPath.iterator(), value);
    }

    /**
     * Sets the value at a given path within a namespace.
     *
     * @param namespace
     * @param keyPathIterator
     * @param value
     */
    private static void set(Object namespace, Iterator<String> keyPathIterator, Object value) {
        if (keyPathIterator == null) {
            throw new NullPointerException();
        }

        if (!keyPathIterator.hasNext()) {
            throw new IllegalArgumentException();
        }

        String key = keyPathIterator.next();

        if (keyPathIterator.hasNext()) {
            set(get(namespace, key), keyPathIterator, value);
        } else {
            set(namespace, key, value);
        }
    }

    /**
     * Sets the value at a given path within a namespace.
     *
     * @param namespace
     * @param key
     * @param value
     */
    @SuppressWarnings("unchecked")
    public static void set(Object namespace, String key, Object value) {
        if (key == null) {
            throw new NullPointerException();
        }

        if (namespace instanceof List<?>) {
            List<Object> list = (List<Object>)namespace;
            list.set(Integer.parseInt(key), value);
        } else if (namespace != null) {
            Map<String, Object> map;
            if (namespace instanceof Map<?, ?>) {
                map = (Map<String, Object>)namespace;
            } else {
                map = new BeanAdapter(namespace);
            }

            map.put(key, value);
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Tests the existence of a path within a namespace.
     *
     * @param namespace
     * @param keyPath
     *
     * @return
     * <tt>true</tt> if the path exists; <tt>false</tt>, otherwise.
     */
    public static boolean isDefined(Object namespace, KeyPath keyPath) {
        if (keyPath == null) {
            throw new NullPointerException();
        }

        return isDefined(namespace, keyPath.iterator());
    }

    /**
     * Tests the existence of a path within a namespace.
     *
     * @param namespace
     * @param keyPathIterator
     *
     * @return
     * <tt>true</tt> if the path exists; <tt>false</tt>, otherwise.
     */
    private static boolean isDefined(Object namespace, Iterator<String> keyPathIterator) {
        if (keyPathIterator == null) {
            throw new NullPointerException();
        }

        if (!keyPathIterator.hasNext()) {
            throw new IllegalArgumentException();
        }

        String key = keyPathIterator.next();

        boolean defined;
        if (keyPathIterator.hasNext()) {
            defined = isDefined(get(namespace, key), keyPathIterator);
        } else {
            defined = isDefined(namespace, key);
        }

        return defined;
    }

    /**
     * Tests the existence of a key within a namespace.
     *
     * @param namespace
     * @param key
     *
     * @return
     * <tt>true</tt> if the key exists; <tt>false</tt>, otherwise.
     */
    @SuppressWarnings("unchecked")
    public static boolean isDefined(Object namespace, String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        boolean defined;
        if (namespace instanceof List<?>) {
            List<Object> list = (List<Object>)namespace;
            defined = Integer.parseInt(key) < list.size();
        } else if (namespace != null) {
            Map<String, Object> map;
            if (namespace instanceof Map<?, ?>) {
                map = (Map<String, Object>)namespace;
            } else {
                map = new BeanAdapter(namespace);
            }

            defined = map.containsKey(key);
        } else {
            defined = false;
        }

        return defined;
    }

    /**
     * Creates an addition or concatenation expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression add(Expression left, Expression right) {
        return new BinaryExpression(left, right, (leftValue, rightValue) -> {
            Object value;
            if (leftValue instanceof String || rightValue instanceof String) {
                value = leftValue.toString().concat(rightValue.toString());
            } else {
                Number leftNumber = (Number)leftValue;
                Number rightNumber = (Number)rightValue;

                if (leftNumber instanceof Double || rightNumber instanceof Double) {
                    value = leftNumber.doubleValue() + rightNumber.doubleValue();
                } else if (leftNumber instanceof Float || rightNumber instanceof Float) {
                    value = leftNumber.floatValue() + rightNumber.floatValue();
                } else if (leftNumber instanceof Long || rightNumber instanceof Long) {
                    value = leftNumber.longValue() + rightNumber.longValue();
                } else if (leftNumber instanceof Integer || rightNumber instanceof Integer) {
                    value = leftNumber.intValue() + rightNumber.intValue();
                } else if (leftNumber instanceof Short || rightNumber instanceof Short) {
                    value = leftNumber.shortValue() + rightNumber.shortValue();
                } else if (leftNumber instanceof Byte || rightNumber instanceof Byte) {
                    value = leftNumber.byteValue() + rightNumber.byteValue();
                } else {
                    throw new UnsupportedOperationException();
                }
            }

            return value;
        });
    }

    /**
     * Creates an addition or concatenation expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression add(Expression left, Object right) {
        return add(left, new LiteralExpression(right));
    }

    /**
     * Creates an addition or concatenation expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression add(Object left, Expression right) {
        return add(new LiteralExpression(left), right);
    }

    /**
     * Creates an addition or concatenation expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression add(Object left, Object right) {
        return add(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a subtraction expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression subtract(Expression left, Expression right) {
        return new BinaryExpression<Number, Number>(left, right, (Number leftValue, Number rightValue) -> {
            Number value;
            if (leftValue instanceof Double || rightValue instanceof Double) {
                value = leftValue.doubleValue() - rightValue.doubleValue();
            } else if (leftValue instanceof Float || rightValue instanceof Float) {
                value = leftValue.floatValue() - rightValue.floatValue();
            } else if (leftValue instanceof Long || rightValue instanceof Long) {
                value = leftValue.longValue() - rightValue.longValue();
            } else if (leftValue instanceof Integer || rightValue instanceof Integer) {
                value = leftValue.intValue() - rightValue.intValue();
            } else if (leftValue instanceof Short || rightValue instanceof Short) {
                value = leftValue.shortValue() - rightValue.shortValue();
            } else if (leftValue instanceof Byte || rightValue instanceof Byte) {
                value = leftValue.byteValue() - rightValue.byteValue();
            } else {
                throw new UnsupportedOperationException();
            }

            return value;
        });
    }

    /**
     * Creates a subtraction expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression subtract(Expression left, Number right) {
        return subtract(left, new LiteralExpression(right));
    }

    /**
     * Creates a subtraction expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression subtract(Number left, Expression right) {
        return subtract(new LiteralExpression(left), right);
    }

    /**
     * Creates a subtraction expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression subtract(Number left, Number right) {
        return subtract(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a multiplication expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression multiply(Expression left, Expression right) {
        return new BinaryExpression<Number, Number>(left, right, (Number leftValue, Number rightValue) -> {

            Number value;
            if (leftValue instanceof Double || rightValue instanceof Double) {
                value = leftValue.doubleValue() * rightValue.doubleValue();
            } else if (leftValue instanceof Float || rightValue instanceof Float) {
                value = leftValue.floatValue() * rightValue.floatValue();
            } else if (leftValue instanceof Long || rightValue instanceof Long) {
                value = leftValue.longValue() * rightValue.longValue();
            } else if (leftValue instanceof Integer || rightValue instanceof Integer) {
                value = leftValue.intValue() * rightValue.intValue();
            } else if (leftValue instanceof Short || rightValue instanceof Short) {
                value = leftValue.shortValue() * rightValue.shortValue();
            } else if (leftValue instanceof Byte || rightValue instanceof Byte) {
                value = leftValue.byteValue() * rightValue.byteValue();
            } else {
                throw new UnsupportedOperationException();
            }

            return value;
        });
    }

    /**
     * Creates a multiplication expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression multiply(Expression left, Number right) {
        return multiply(left, new LiteralExpression(right));
    }

    /**
     * Creates a multiplication expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression multiply(Number left, Expression right) {
        return multiply(new LiteralExpression(left), right);
    }

    /**
     * Creates a multiplication expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression multiply(Number left, Number right) {
        return multiply(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a division expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression divide(Expression left, Expression right) {
        return new BinaryExpression<Number, Number>(left, right, (Number leftValue, Number rightValue) -> {

            Number value;
            if (leftValue instanceof Double || rightValue instanceof Double) {
                value = leftValue.doubleValue() / rightValue.doubleValue();
            } else if (leftValue instanceof Float || rightValue instanceof Float) {
                value = leftValue.floatValue() / rightValue.floatValue();
            } else if (leftValue instanceof Long || rightValue instanceof Long) {
                value = leftValue.longValue() / rightValue.longValue();
            } else if (leftValue instanceof Integer || rightValue instanceof Integer) {
                value = leftValue.intValue() / rightValue.intValue();
            } else if (leftValue instanceof Short || rightValue instanceof Short) {
                value = leftValue.shortValue() / rightValue.shortValue();
            } else if (leftValue instanceof Byte || rightValue instanceof Byte) {
                value = leftValue.byteValue() / rightValue.byteValue();
            } else {
                throw new UnsupportedOperationException();
            }

            return value;
        });
    }

    /**
     * Creates a division expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression divide(Expression left, Number right) {
        return divide(left, new LiteralExpression(right));
    }

    /**
     * Creates a division expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression divide(Number left, Expression<Number> right) {
        return divide(new LiteralExpression(left), right);
    }

    /**
     * Creates a division expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression divide(Number left, Number right) {
        return divide(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a modulus expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression modulo(Expression left, Expression right) {
        return new BinaryExpression<Number, Number>(left, right, (Number leftValue, Number rightValue) -> {

                Number value;
                if (leftValue instanceof Double || rightValue instanceof Double) {
                    value = leftValue.doubleValue() % rightValue.doubleValue();
                } else if (leftValue instanceof Float || rightValue instanceof Float) {
                    value = leftValue.floatValue() % rightValue.floatValue();
                } else if (leftValue instanceof Long || rightValue instanceof Long) {
                    value = leftValue.longValue() % rightValue.longValue();
                } else if (leftValue instanceof Integer || rightValue instanceof Integer) {
                    value = leftValue.intValue() % rightValue.intValue();
                } else if (leftValue instanceof Short || rightValue instanceof Short) {
                    value = leftValue.shortValue() % rightValue.shortValue();
                } else if (leftValue instanceof Byte || rightValue instanceof Byte) {
                    value = leftValue.byteValue() % rightValue.byteValue();
                } else {
                    throw new UnsupportedOperationException();
                }

                return value;
            });
    }

    /**
     * Creates a modulus expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression modulo(Expression<Number> left, Number right) {
        return modulo(left, new LiteralExpression(right));
    }

    /**
     * Creates a modulus expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression modulo(Number left, Expression<Number> right) {
        return modulo(new LiteralExpression(left), right);
    }

    /**
     * Creates a modulus expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression modulo(Number left, Number right) {
        return modulo(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates an equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression equalTo(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (Comparable leftValue, Comparable rightValue) ->
                leftValue.compareTo(rightValue) == 0
            );
    }

    /**
     * Creates an equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression equalTo(Expression left, Object right) {
        return equalTo(left, new LiteralExpression(right));
    }

    /**
     * Creates an equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression equalTo(Object left, Expression right) {
        return equalTo(new LiteralExpression(left), right);
    }

    /**
     * Creates an equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression equalTo(Object left, Object right) {
        return equalTo(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates an inverse equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression notEqualTo(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (leftValue, rightValue) ->
                 leftValue.compareTo(rightValue) != 0
        );
    }

    /**
     * Creates an inverse equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression notEqualTo(Expression left, Object right) {
        return notEqualTo(left, new LiteralExpression(right));
    }

    /**
     * Creates an inverse equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression notEqualTo(Object left, Expression right) {
        return notEqualTo(new LiteralExpression(left), right);
    }

    /**
     * Creates an inverse equality expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression notEqualTo(Object left, Object right) {
        return notEqualTo(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a "greater-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThan(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue.compareTo(rightValue) > 0
        );
    }

    /**
     * Creates a "greater-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThan(Expression left, Object right) {
        return greaterThan(left, new LiteralExpression(right));
    }

    /**
     * Creates a "greater-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThan(Object left, Expression right) {
        return greaterThan(new LiteralExpression(left), right);
    }

    /**
     * Creates a "greater-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThan(Object left, Object right) {
        return greaterThan(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a "greater-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThanOrEqualTo(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue.compareTo(rightValue) >= 0
        );
    }

    /**
     * Creates a "greater-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThanOrEqualTo(Expression left, Object right) {
        return greaterThanOrEqualTo(left, new LiteralExpression(right));
    }

    /**
     * Creates a "greater-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThanOrEqualTo(Object left, Expression right) {
        return greaterThanOrEqualTo(new LiteralExpression(left), right);
    }

    /**
     * Creates a "greater-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression greaterThanOrEqualTo(Object left, Object right) {
        return greaterThanOrEqualTo(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a "less-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThan(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue.compareTo(rightValue) < 0
        );
    }

    /**
     * Creates a "less-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThan(Expression left, Object right) {
        return lessThan(left, new LiteralExpression(right));
    }

    /**
     * Creates a "less-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThan(Object left, Expression right) {
        return lessThan(new LiteralExpression(left), right);
    }

    /**
     * Creates a "less-than" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThan(Object left, Object right) {
        return lessThan(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a "less-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThanOrEqualTo(Expression left, Expression right) {
        return new BinaryExpression<Comparable, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue.compareTo(rightValue) <= 0
        );
    }

    /**
     * Creates a "less-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThanOrEqualTo(Expression left, Object right) {
        return lessThanOrEqualTo(left, new LiteralExpression(right));
    }

    /**
     * Creates a "less-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThanOrEqualTo(Object left, Expression right) {
        return lessThanOrEqualTo(new LiteralExpression(left), right);
    }

    /**
     * Creates a "less-than-or-equal-to" comparison expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression lessThanOrEqualTo(Object left, Object right) {
        return lessThanOrEqualTo(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a boolean "and" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression and(Expression left, Expression right) {
        return new BinaryExpression<Boolean, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue && rightValue
        );
    }

    /**
     * Creates a boolean "and" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression and(Expression left, Boolean right) {
        return and(left, new LiteralExpression(right));
    }

    /**
     * Creates a boolean "and" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression and(Boolean left, Expression right) {
        return and(new LiteralExpression(left), right);
    }

    /**
     * Creates a boolean "and" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression and(Boolean left, Boolean right) {
        return and(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a boolean "or" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression or(Expression left, Expression right) {
        return new BinaryExpression<Boolean, Boolean>(left, right, (leftValue, rightValue) ->
                leftValue || rightValue
        );
    }

    /**
     * Creates a boolean "or" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression or(Expression left, Boolean right) {
        return or(left, new LiteralExpression(right));
    }

    /**
     * Creates a boolean "or" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression or(Boolean left, Expression right) {
        return or(new LiteralExpression(left), right);
    }

    /**
     * Creates a boolean "or" expression.
     *
     * @param left
     * @param right
     */
    public static BinaryExpression or(Boolean left, Boolean right) {
        return or(new LiteralExpression(left), new LiteralExpression(right));
    }

    /**
     * Creates a numeric negation expression.
     *
     * @param operand
     */
    public static UnaryExpression negate(Expression operand) {
        return new UnaryExpression<Number, Number>(operand, (value) -> {
                Class<? extends Number> type = value.getClass();

                if (type == Byte.class) {
                    return -value.byteValue();
                } else if (type == Short.class) {
                    return -value.shortValue();
                } else if (type == Integer.class) {
                    return -value.intValue();
                } else if (type == Long.class) {
                    return -value.longValue();
                } else if (type == Float.class) {
                    return -value.floatValue();
                } else if (type == Double.class) {
                    return -value.doubleValue();
                } else {
                    throw new UnsupportedOperationException();
                }

            });
    }

    /**
     * Creates a numeric negation expression.
     *
     * @param operand
     */
    public static UnaryExpression negate(Number operand) {
        return negate(new LiteralExpression(operand));
    }

    /**
     * Creates a boolean "not" expression.
     *
     * @param operand
     */
    public static UnaryExpression not(Expression operand) {
        return new UnaryExpression<Boolean, Boolean>(operand, (value) -> !value);
    }

    /**
     * Creates a boolean "not" expression.
     *
     * @param operand
     */
    public static UnaryExpression not(Boolean operand) {
        return not(new LiteralExpression(operand));
    }

    /**
     * Parses a string representation of an expression into an expression
     * tree.
     *
     * @param value
     * The string representation of the expression.
     */
    public static Expression valueOf(String value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Parser parser = new Parser();
        Expression expression;
        try {
            expression = parser.parse(new StringReader(value));
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }

        return expression;
    }
}
