package org.mvel2.util;

import org.mvel2.ast.ASTNode;
import org.mvel2.ast.EndOfStatement;
import org.mvel2.ast.OperatorNode;

import static org.mvel2.Operator.*;

public class ASTBinaryTree {
    private final ASTNode root;
    private ASTBinaryTree left;
    private ASTBinaryTree right;

    public ASTBinaryTree(ASTNode node) {
        this.root = node;
    }

    public ASTBinaryTree append(ASTNode node) {
        if (comparePrecedence(root, node) >= 0) {
            ASTBinaryTree tree = new ASTBinaryTree(node);
            tree.left = this;
            return tree;
        } else {
            if (left == null) throw new RuntimeException("Missing left node");
            if (right == null) {
                right = new ASTBinaryTree(node);
            } else {
                right = right.append(node);
            }
            return this;
        }
    }

    public Class<?> getReturnType(boolean strongTyping) {
        if (!(root instanceof OperatorNode)) return root.getEgressType();
        if (left == null || right == null) throw new RuntimeException("Malformed expression");
        Class<?> leftType = left.getReturnType(strongTyping);
        Class<?> rightType = right.getReturnType(strongTyping);
        switch (root.getOperator()) {
            case CONTAINS:
            case SOUNDEX:
            case INSTANCEOF:
            case SIMILARITY:
            case REGEX:
                return Boolean.class;
            case ADD:
                if (leftType.equals(String.class) || rightType.equals(String.class)) return String.class;
            case SUB:
            case MULT:
            case DIV:
                if (strongTyping && !CompatibilityStrategy.areEqualityCompatible(leftType, rightType))
                    throw new RuntimeException("Associative operation requires compatible types. Found " + leftType + " and " + rightType);
                return leftType == rightType && (leftType.isPrimitive() || Number.class.isAssignableFrom(leftType)) ? leftType : Double.class;
            case MOD:
                if (strongTyping && !CompatibilityStrategy.areEqualityCompatible(leftType, rightType))
                    throw new RuntimeException("Associative operation requires compatible types. Found " + leftType + " and " + rightType);
                return Integer.class;
            case TERNARY_ELSE:
                if (strongTyping && !CompatibilityStrategy.areEqualityCompatible(leftType, rightType))
                    throw new RuntimeException("Associative operation requires compatible types. Found " + leftType + " and " + rightType);
                return leftType;
            case EQUAL:
            case NEQUAL:
                if (strongTyping && !CompatibilityStrategy.areEqualityCompatible(leftType, rightType))
                    throw new RuntimeException("Comparison operation requires compatible types. Found " + leftType + " and " + rightType);
                return Boolean.class;
            case LTHAN:
            case LETHAN:
            case GTHAN:
            case GETHAN:
                if (strongTyping && !CompatibilityStrategy.areComparisonCompatible(leftType, rightType))
                    throw new RuntimeException("Comparison operation requires compatible types. Found " + leftType + " and " + rightType);
                return Boolean.class;
            case AND:
            case OR:
                if (strongTyping) {
                    if (leftType != Boolean.class && leftType != Boolean.TYPE)
                        throw new RuntimeException("Left side of logical operation is not of type boolean. Found " + leftType);
                    if (rightType != Boolean.class && rightType != Boolean.TYPE)
                        throw new RuntimeException("Right side of logical operation is not of type boolean. Found " + rightType);
                }
                return Boolean.class;
            case TERNARY:
                if (strongTyping && leftType != Boolean.class && leftType != Boolean.TYPE)
                    throw new RuntimeException("Condition of ternary operator is not of type boolean. Found " + leftType);
                return rightType;
        }
        // TODO: should throw new RuntimeException("Unknown operator");
        // it doesn't because I am afraid I am not covering all the OperatorNode types
        return root.getEgressType();
    }

    private int comparePrecedence(ASTNode node1, ASTNode node2) {
        if (!(node1 instanceof OperatorNode) && !(node2 instanceof OperatorNode)) return 0;
        if (node1 instanceof OperatorNode && node2 instanceof OperatorNode) {
            return PTABLE[node1.getOperator()] - PTABLE[node2.getOperator()];
        }
        return node1 instanceof OperatorNode ? -1 : 1;
    }

    public static ASTBinaryTree buildTree(ASTIterator input) {
        ASTIterator iter = new ASTLinkedList(input.firstNode());
        ASTBinaryTree tree = new ASTBinaryTree(iter.nextNode());
        while (iter.hasMoreNodes()) {
            ASTNode node = iter.nextNode();
            if (node instanceof EndOfStatement) {
                if (iter.hasMoreNodes()) tree = new ASTBinaryTree(iter.nextNode());
            } else {
                tree = tree.append(node);
            }
        }
        return tree;
    }
}
