import java.util.ArrayList;

/**
 * Derived class that represents an expression in the SILLY language.
 *
 * @author Dave Reed
 * @version 1/26/20
 */
public class Expression {
    private ArrayList<Token> tokenList;

    /**
     * Creates an expression from the specified TokenStream.
     *   @param input the TokenStream from which the program is read
     *   @throws Exception if the expression is malformed
     */
    public Expression(TokenStream input) throws Exception {
        this.tokenList = new ArrayList<Token>();

        if (input.lookAhead().getType() == Token.Type.UNARY_OP) {
            this.tokenList.add(null);
            this.tokenList.add(input.next());
            this.tokenList.add(input.next());
        } else {
            this.tokenList.add(input.next());
            while (input.lookAhead().getType() == Token.Type.BINARY_OP) {
                this.tokenList.add(input.next());
                this.tokenList.add(input.next());
            }
        }

        for (int i = 0; i < this.tokenList.size(); i += 2) {
            Token tok = this.tokenList.get(i);
            if (tok != null &&
                tok.getType() != Token.Type.IDENTIFIER &&
                tok.getType() != Token.Type.INTEGER_LITERAL && 
                tok.getType() != Token.Type.STRING_LITERAL && 
                tok.getType() != Token.Type.BOOLEAN_LITERAL) {
                throw new Exception("SYNTAX ERROR: malformed expression");
            }
        }
    }

    /**
     * Evaluates the current expression.
     *   @return the value represented by the expression
     *   @throws Exception if any type mismatches occur
     */
    public DataValue evaluate() throws Exception {
        if (this.tokenList.size() == 1) {
            return this.getValue(this.tokenList.get(0));
        } else if (this.tokenList.get(1).getType() == Token.Type.UNARY_OP) {
            if (this.tokenList.get(1).toString().equals("not")) {
                DataValue t = this.getValue(this.tokenList.get(2));
                if (t.getType() == DataValue.Type.BOOLEAN_VALUE) {
                    boolean b2 = ((Boolean) (t.getValue()));
                    return new BooleanValue(!b2);
                }
            } 
            throw new Exception("RUNTIME ERROR: illegal unary operand"); 
        } else {
            DataValue result = this.getValue(this.tokenList.get(0));
            for (int i = 1; i < this.tokenList.size(); i += 2) {
                Token op = this.tokenList.get(i);
                DataValue rhsValue = this.getValue(this.tokenList.get(i + 1));

                if (result.getType() == rhsValue.getType()) {
                    if (op.toString().equals("==")) {
                        result = new BooleanValue(result.compareTo(rhsValue) == 0);
                        continue;
                    } else if (op.toString().equals("!=")) {
                        result = new BooleanValue(result.compareTo(rhsValue) != 0);
                        continue;
                    } else if (op.toString().equals(">")) {
                        result = new BooleanValue(result.compareTo(rhsValue) > 0);
                        continue;
                    } else if (op.toString().equals("<")) {
                        result = new BooleanValue(result.compareTo(rhsValue) < 0);
                        continue;
                    } else if (result.getType() == DataValue.Type.INTEGER_VALUE) {
                        int num1 = ((Integer) (result.getValue()));
                        int num2 = ((Integer) (rhsValue.getValue()));

                        if (op.toString().equals("+")) {
                            result = new IntegerValue(num1 + num2);
                            continue;
                        } else if (op.toString().equals("-")) {
                            result = new IntegerValue(num1 - num2);
                            continue;
                        } else if (op.toString().equals("*")) {
                            result = new IntegerValue(num1 * num2);
                            continue;
                        } else if (op.toString().equals("/")) {
                            result = new IntegerValue(num1 / num2);
                            continue;
                        } 
                    } else if (result.getType() == DataValue.Type.BOOLEAN_VALUE) {
                        boolean b1 = ((Boolean) (result.getValue()));
                        boolean b2 = ((Boolean) (rhsValue.getValue()));

                        if (op.toString().equals("and")) {
                            result = new BooleanValue(b1 && b2);
                            continue;
                        }
                    }
                }
                throw new Exception("RUNTIME ERROR: illegal binary operand(s)");                    
            }
            return result;
        }
    }

    /**
     * Converts the current expression into a String.
     *   @return the String representation of this expression
     */
    public String toString() {
        String result = "";
        if (this.tokenList.get(0) != null) {
            result = this.tokenList.get(0).toString();
        }
        for (int i = 1; i < this.tokenList.size(); i++) {
            result += " " + this.tokenList.get(i).toString();
        }
        return result;
    }

    ////////////////////////////////////////////////////////////////////////////
    
    /**
     * Private method that determines the data value of an identifier or literal.
     * @param tok the token representing the identifier/literal
     * @return the DataValue for that identifier/literal
     * @throws Exception if attempt to lookup an undeclared variable
     */
    private DataValue getValue(Token tok) throws Exception {
        if (tok.getType() == Token.Type.IDENTIFIER) {
            return Interpreter.MEMORY.lookupVariable(tok);
        } else if (tok.getType() == Token.Type.INTEGER_LITERAL) {
            return new IntegerValue(Integer.parseInt(tok.toString()));
        } else if (tok.getType() == Token.Type.STRING_LITERAL) {
            return new StringValue(tok.toString());
        } else if (tok.getType() == Token.Type.BOOLEAN_LITERAL) {
            return new BooleanValue(Boolean.valueOf(tok.toString()));
        }
        return null;
    }
}