// Reads the input box for a RPN expression
// Calculates result
// Outputs it as nicely formatted boxes.
function calculateFromInput() {
	var expr = document.getElementById("input").value;
	var output = document.getElementById("output")

	var result = calculateRPN(expr.split(" "));
		
	output.innerHTML = ""; // Clear the output div

	result.stack.forEach(function(num) {
		num = num.toString().replace("NaN", "Error");
		var box = createBox(num);
		box.style["max-width"] = num.length + "em";
		output.appendChild(box);
	});

	if (result.errors.length > 0) { // If errors exist output them separated by line breaks.
			result.errors.forEach(error => {
				output.appendChild(document.createTextNode(error.toString()))
				output.appendChild(document.createElement("br"))
			})
	}
}

function add(x, y) {
	return x + y;
}

function multiply(x, y) {
	return x * y;
}

function divide(x, y) {
	return x / y;
}

function flip(x, y) {
	return [y, x];
}

function subtract(x, y) {
	return x - y;
}

function sum(vals) {
	var acc = 0;

	vals.forEach(function(el) {
		acc += el;
	});

	return acc;
}

function range(low, high) {
	var r = [];

	for (var i = low; i <= high; i++) {
		r.push(i);
	}
	
	return r;
}

function createBox(contents) {
	var el = document.createElement("div");
	el.setAttribute("class", "stack-box");

	el.innerText = contents;

	return el;
}

// Takes a two-argument function and lifts it to a binary stack op
function binaryOp(fun) {
	return function(stack) {
		var x = stack.pop();
		var y = stack.pop();

		stack.push(fun(y, x));
		return stack;
	}
}

// Takes a function and lifts it to a unary op using a stack. Returns new stack.
function unaryOp(fun) {
	return function(stack) {
		var x = stack.pop();
		
		stack.push(fun(x));
		return stack;
	}
}

// Takes a function and lifts it to an op which takes the entire stack and reduces it to one value. Returns new stack.
function greedyOp(fun) {
	return function(stack) {
		return [fun(stack)];
	}
}

// Lifs to an operator which takes two values and adds an array of them to the stack. Returns a new stack.
function binaryMultioutOp(fun) {
	return function(stack) {
		var x = stack.pop();
		var y = stack.pop();
		
		return stack.concat(fun(y, x));
	}
}

function calculateRPN(tokens) {
	var stack = [];
	var errors = [];

	for (var i = 0; i < tokens.length; i++) {
		var token = tokens[i];

		switch (token) {
		case "+":
			stack = binaryOp(add)(stack);
			break;
		case "*":
			stack = binaryOp(multiply)(stack);
			break;
		case "/":
			stack = binaryOp(divide)(stack);
			break;
		case "-":
			stack = binaryOp(subtract)(stack);
			break;
		case "range":
			stack = binaryMultioutOp(range)(stack);
			break;
		case "flip":
			stack = binaryMultioutOp(flip)(stack);
			break;
		case "sum":
			stack = greedyOp(sum)(stack);
			break;
		case "": // This may be entered by accident.
			break;
		default:
			var parsed = parseFloat(token);

			if (parsed === parsed) { // check for NaNs from a failed parse
				stack.push(parsed);
			} else {
				errors.push("Token '" + token + "' (#" + (i + 1) + ")" + " invalid");
			}
		}
	}

	return {stack: stack, errors: errors};
}
