package cd.ir;

import cd.ir.Ast.BinaryOp;
import cd.ir.Ast.BooleanConst;
import cd.ir.Ast.Expr;
import cd.ir.Ast.UnaryOp;
import cd.ir.Symbol.VariableSymbol;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static cd.Config.FALSE;
import static cd.Config.TRUE;

/**
 * A visitor that only visits {@link Expr} nodes.
 * calculates the compile time value of an rhs in an assignment statement or the boolean value of a condition
 */
public class CompileTimeEvaluator extends ExprVisitor<Integer, Map<VariableSymbol, Integer>> {

	// if the expression contains non constant variables, field or index; or if the expression is a read()
	// no compile Time value can be computed
	private boolean failedToEvaluate;

	public Optional<Integer> calc(Ast.Expr ast) {
		return calc(ast, new HashMap<>());
	}

	public Optional<Integer> calc(Ast.Expr ast, Map<VariableSymbol, Integer> constantMap) {
		try {
			failedToEvaluate = false;
			Integer result = visit(ast, constantMap);
			if (failedToEvaluate)
				return Optional.empty();
			else
				return Optional.of(result);
		} catch (ArithmeticException e) {
			return Optional.empty();
		}
	}

	@Override
	protected Integer dfltExpr(Expr ast, Map<VariableSymbol, Integer> arg) {
		failedToEvaluate = true;
		return Integer.MIN_VALUE;
	}


	@Override
	public Integer binaryOp(BinaryOp ast, Map<VariableSymbol, Integer> arg) {
		boolean tempResult;
		int left = visit(ast.left(), arg);
		int right = visit(ast.right(), arg);

		switch (ast.operator) {
			case B_TIMES:
				return left * right;
			case B_PLUS:
				return left + right;
			case B_MINUS:
				return left - right;
			case B_DIV:
				return left / right;
			case B_MOD:
				return left % right;
			case B_AND:
				tempResult = (left == TRUE) && (right == TRUE);
				break;
			case B_OR:
				tempResult = (left == TRUE) || (right == TRUE);
				break;
			case B_EQUAL:
				tempResult = left == right;
				break;
			case B_NOT_EQUAL:
				tempResult = left != right;
				break;
			case B_LESS_THAN:
				tempResult = left < right;
				break;
			case B_LESS_OR_EQUAL:
				tempResult = left <= right;
				break;
			case B_GREATER_THAN:
				tempResult = left > right;
				break;
			case B_GREATER_OR_EQUAL:
				tempResult = left >= right;
				break;
			default:
				throw new RuntimeException("Invalid binary operator");
		}
		return tempResult ? TRUE : FALSE;
	}


	@Override
	public Integer unaryOp(UnaryOp ast, Map<VariableSymbol, Integer> arg) {
		int result = visit(ast.arg(), arg);

		switch (ast.operator) {
			case U_PLUS:
				return result;
			case U_MINUS:
				return -result;
			case U_BOOL_NOT:
				return result == FALSE ? TRUE : FALSE;
			default:
				throw new RuntimeException("Invalid unary operator");
		}
	}

	@Override
	public Integer booleanConst(BooleanConst ast, Map<VariableSymbol, Integer> arg) {
		return ast.value ? TRUE : FALSE;
	}

	@Override
	public Integer intConst(Ast.IntConst ast, Map<VariableSymbol, Integer> arg) {
		return ast.value;
	}

	// check if a given Variable has a constant value
	@Override
	public Integer var(Ast.Var ast, Map<VariableSymbol, Integer> arg) {
		if (arg.containsKey(ast.sym)) {
			return arg.get(ast.sym);
		} else {
			failedToEvaluate = true;
			return Integer.MIN_VALUE;
		}
	}

}
