
/**
 * Derived class that represents an expression in the SILLY language.
 *
 * @author Dave Reed
 * @version 2/8/17
 */
public class Expression {

    private Term term;
    private Token op;
    private Expression expr;

    /**
     * Creates an expression from the specified TokenStream.
     *
     * @param input the TokenStream from which the program is read
     */
    public Expression(TokenStream input) throws Exception {
        if (input.lookAhead().getType() == Token.Type.UNARY_OP) {
            this.op = input.next();
            this.term = new Term(input);
        } else {
            this.term = new Term(input);
            if (input.lookAhead().getType() == Token.Type.BINARY_OP) {
                this.op = input.next();
                this.expr = new Expression(input);
            }
        }
    }

    /**
     * Evaluates the current expression involving integers and/or variables.
     *
     * @param bindings the current variable and subroutine bindings
     * @return the value represented by the expression
     */
    public DataValue evaluate() throws Exception {
        if (this.op == null) {
            return this.term.evaluate();
        } else if (this.op.getType() == Token.Type.BINARY_OP) {
            DataValue lhsValue = this.term.evaluate();
            DataValue rhsValue = this.expr.evaluate();

            if (this.op.toString().equals("+")
                    && (lhsValue.getType() == Token.Type.STRING
                    || rhsValue.getType() == Token.Type.STRING)) {
                lhsValue = new StringValue("\"" + lhsValue.toString().replace("\"", "") + "\"");
                rhsValue = new StringValue("\"" + rhsValue.toString().replace("\"", "") + "\"");
            }

            if (lhsValue.getType() == rhsValue.getType()) {
                if (lhsValue.getType() == Token.Type.INTEGER) {
                    int i1 = ((Integer) (lhsValue.getValue())).intValue();
                    int i2 = ((Integer) (rhsValue.getValue())).intValue();

                    if (this.op.toString().equals("+")) {
                        return new IntegerValue(i1 + i2);
                    } else if (this.op.toString().equals("-")) {
                        return new IntegerValue(i1 - i2);
                    } else if (this.op.toString().equals("*")) {
                        return new IntegerValue(i1 * i2);
                    } else if (this.op.toString().equals("/")) {
                        return new IntegerValue(i1 / i2);
                    } else if (this.op.toString().equals("%")) {
                        return new IntegerValue(i1 % i2);
                    }
                } else if (lhsValue.getType() == Token.Type.STRING) {
                    String str1 = lhsValue.toString();
                    String str2 = rhsValue.toString();

                    if (this.op.toString().equals("+")) {
                        return new StringValue(str1.substring(0, str1.length() - 1)
                                + str2.substring(1, str2.length()));
                    }
                }
            }
        }
        throw new Exception("RUNTIME ERROR: illegal operand(s) for " + this.op);
    }

        /**
         * Converts the current expression into a String.
         *
         * @return the String representation of this expression
         */
    

    public String toString() {
        if (this.op == null) {
            return this.term.toString();
        } else if (this.op.getType() == Token.Type.UNARY_OP) {
            return this.op + " " + this.term;
        } else {
            return this.term + " " + this.op + " " + this.expr;
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    /**
     * Inner class that represents a term in the SILLY language.
     *
     * @author Dave Reed
     * @version 2/8/17
     */
    public class Term {
        private Token tok;
        private Expression expr;

        /**
         * Creates a term from the specified TokenStream.
         *
         * @param input the TokenStream from which the program is read
         */
        public Term(TokenStream input) throws Exception {
            this.tok = input.next();
            if (this.tok.toString().equals("(")) {
                this.expr = new Expression(input);
                if (!input.next().toString().equals(")")) {
                    throw new Exception("SYNTAX ERROR: Malformed term");
                }
            } else if (this.tok.getType() != Token.Type.INTEGER
                    && this.tok.getType() != Token.Type.STRING
                    && this.tok.getType() != Token.Type.BOOLEAN
                    && this.tok.getType() != Token.Type.IDENTIFIER) {
                throw new Exception("SYNTAX ERROR: Malformed term");
            }
        }

        /**
         * Evaluates the current term involving integers and/or variables.
         *
         * @param bindings the current variable and subroutine bindings
         * @return the value represented by the term
         */
        public DataValue evaluate() throws Exception {
            if (this.tok.toString().equals("(")) {
                return this.expr.evaluate();
            } else if (this.tok.getType() == Token.Type.IDENTIFIER) {
                return Interpreter.MEMORY.lookupVariable(this.tok);
            } else if (this.tok.getType() == Token.Type.INTEGER) {
                return new IntegerValue(Integer.parseInt(this.tok.toString()));
            } else if (this.tok.getType() == Token.Type.STRING) {
                return new StringValue(this.tok.toString());
            } else if (this.tok.getType() == Token.Type.BOOLEAN) {
                return new BooleanValue(Boolean.valueOf(this.tok.toString()));
            } else {
                throw new Exception("RUNTIME ERROR: Invalid data type");
            }
        }

        /**
         * Converts the current term into a String.
         *
         * @return the String representation of this term
         */
        public String toString() {
            if (this.tok.toString().equals("(")) {
                return "(" + this.expr + ")";
            } else {
                return this.tok.toString();
            }
        }
    }
}
