/*******************************************************************************
 * Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor.
 * Copyright (c) 2011 The OpenNMS Group, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *******************************************************************************/

package org.jrobin.data;

import org.jrobin.core.RrdException;
import org.jrobin.core.Util;

import java.util.Calendar;
import java.util.StringTokenizer;

class RpnCalculator {
	private static final byte TKN_VAR = 0;
	private static final byte TKN_NUM = 1;
	private static final byte TKN_PLUS = 2;
	private static final byte TKN_MINUS = 3;
	private static final byte TKN_MULT = 4;
	private static final byte TKN_DIV = 5;
	private static final byte TKN_MOD = 6;
	private static final byte TKN_SIN = 7;
	private static final byte TKN_COS = 8;
	private static final byte TKN_LOG = 9;
	private static final byte TKN_EXP = 10;
	private static final byte TKN_FLOOR = 11;
	private static final byte TKN_CEIL = 12;
	private static final byte TKN_ROUND = 13;
	private static final byte TKN_POW = 14;
	private static final byte TKN_ABS = 15;
	private static final byte TKN_SQRT = 16;
	private static final byte TKN_RANDOM = 17;
	private static final byte TKN_LT = 18;
	private static final byte TKN_LE = 19;
	private static final byte TKN_GT = 20;
	private static final byte TKN_GE = 21;
	private static final byte TKN_EQ = 22;
	private static final byte TKN_IF = 23;
	private static final byte TKN_MIN = 24;
	private static final byte TKN_MAX = 25;
	private static final byte TKN_LIMIT = 26;
	private static final byte TKN_DUP = 27;
	private static final byte TKN_EXC = 28;
	private static final byte TKN_POP = 29;
	private static final byte TKN_UN = 30;
	private static final byte TKN_UNKN = 31;
	private static final byte TKN_NOW = 32;
	private static final byte TKN_TIME = 33;
	private static final byte TKN_PI = 34;
	private static final byte TKN_E = 35;
	private static final byte TKN_AND = 36;
	private static final byte TKN_OR = 37;
	private static final byte TKN_XOR = 38;
	private static final byte TKN_PREV = 39;
	private static final byte TKN_INF = 40;
	private static final byte TKN_NEGINF = 41;
	private static final byte TKN_STEP = 42;
	private static final byte TKN_YEAR = 43;
	private static final byte TKN_MONTH = 44;
	private static final byte TKN_DATE = 45;
	private static final byte TKN_HOUR = 46;
	private static final byte TKN_MINUTE = 47;
	private static final byte TKN_SECOND = 48;
	private static final byte TKN_WEEK = 49;
	private static final byte TKN_SIGN = 50;
	private static final byte TKN_RND = 51;

	private String rpnExpression;
	private String sourceName;
	private DataProcessor dataProcessor;

	private Token[] tokens;
	private RpnStack stack = new RpnStack();
	private double[] calculatedValues;
	private long[] timestamps;
	private double timeStep;

	RpnCalculator(String rpnExpression, String sourceName, DataProcessor dataProcessor) throws RrdException {
		this.rpnExpression = rpnExpression;
		this.sourceName = sourceName;
		this.dataProcessor = dataProcessor;
		this.timestamps = dataProcessor.getTimestamps();
		this.timeStep = this.timestamps[1] - this.timestamps[0];
		this.calculatedValues = new double[this.timestamps.length];
		StringTokenizer st = new StringTokenizer(rpnExpression, ", ");
		tokens = new Token[st.countTokens()];
		for (int i = 0; st.hasMoreTokens(); i++) {
			tokens[i] = createToken(st.nextToken());
		}
	}

	private Token createToken(String parsedText) throws RrdException {
		Token token = new Token();
		if (Util.isDouble(parsedText)) {
			token.id = TKN_NUM;
			token.number = Util.parseDouble(parsedText);
		}
		else if (parsedText.equals("+")) {
			token.id = TKN_PLUS;
		}
		else if (parsedText.equals("-")) {
			token.id = TKN_MINUS;
		}
		else if (parsedText.equals("*")) {
			token.id = TKN_MULT;
		}
		else if (parsedText.equals("/")) {
			token.id = TKN_DIV;
		}
		else if (parsedText.equals("%")) {
			token.id = TKN_MOD;
		}
		else if (parsedText.equals("SIN")) {
			token.id = TKN_SIN;
		}
		else if (parsedText.equals("COS")) {
			token.id = TKN_COS;
		}
		else if (parsedText.equals("LOG")) {
			token.id = TKN_LOG;
		}
		else if (parsedText.equals("EXP")) {
			token.id = TKN_EXP;
		}
		else if (parsedText.equals("FLOOR")) {
			token.id = TKN_FLOOR;
		}
		else if (parsedText.equals("CEIL")) {
			token.id = TKN_CEIL;
		}
		else if (parsedText.equals("ROUND")) {
			token.id = TKN_ROUND;
		}
		else if (parsedText.equals("POW")) {
			token.id = TKN_POW;
		}
		else if (parsedText.equals("ABS")) {
			token.id = TKN_ABS;
		}
		else if (parsedText.equals("SQRT")) {
			token.id = TKN_SQRT;
		}
		else if (parsedText.equals("RANDOM")) {
			token.id = TKN_RANDOM;
		}
		else if (parsedText.equals("LT")) {
			token.id = TKN_LT;
		}
		else if (parsedText.equals("LE")) {
			token.id = TKN_LE;
		}
		else if (parsedText.equals("GT")) {
			token.id = TKN_GT;
		}
		else if (parsedText.equals("GE")) {
			token.id = TKN_GE;
		}
		else if (parsedText.equals("EQ")) {
			token.id = TKN_EQ;
		}
		else if (parsedText.equals("IF")) {
			token.id = TKN_IF;
		}
		else if (parsedText.equals("MIN")) {
			token.id = TKN_MIN;
		}
		else if (parsedText.equals("MAX")) {
			token.id = TKN_MAX;
		}
		else if (parsedText.equals("LIMIT")) {
			token.id = TKN_LIMIT;
		}
		else if (parsedText.equals("DUP")) {
			token.id = TKN_DUP;
		}
		else if (parsedText.equals("EXC")) {
			token.id = TKN_EXC;
		}
		else if (parsedText.equals("POP")) {
			token.id = TKN_POP;
		}
		else if (parsedText.equals("UN")) {
			token.id = TKN_UN;
		}
		else if (parsedText.equals("UNKN")) {
			token.id = TKN_UNKN;
		}
		else if (parsedText.equals("NOW")) {
			token.id = TKN_NOW;
		}
		else if (parsedText.equals("TIME")) {
			token.id = TKN_TIME;
		}
		else if (parsedText.equals("PI")) {
			token.id = TKN_PI;
		}
		else if (parsedText.equals("E")) {
			token.id = TKN_E;
		}
		else if (parsedText.equals("AND")) {
			token.id = TKN_AND;
		}
		else if (parsedText.equals("OR")) {
			token.id = TKN_OR;
		}
		else if (parsedText.equals("XOR")) {
			token.id = TKN_XOR;
		}
		else if (parsedText.equals("PREV")) {
			token.id = TKN_PREV;
			token.variable = sourceName;
			token.values = calculatedValues;
		}
		else if (parsedText.startsWith("PREV(") && parsedText.endsWith(")")) {
			token.id = TKN_PREV;
			token.variable = parsedText.substring(5, parsedText.length() - 1);
			token.values = dataProcessor.getValues(token.variable);
		}
		else if (parsedText.equals("INF")) {
			token.id = TKN_INF;
		}
		else if (parsedText.equals("NEGINF")) {
			token.id = TKN_NEGINF;
		}
		else if (parsedText.equals("STEP")) {
			token.id = TKN_STEP;
		}
		else if (parsedText.equals("YEAR")) {
			token.id = TKN_YEAR;
		}
		else if (parsedText.equals("MONTH")) {
			token.id = TKN_MONTH;
		}
		else if (parsedText.equals("DATE")) {
			token.id = TKN_DATE;
		}
		else if (parsedText.equals("HOUR")) {
			token.id = TKN_HOUR;
		}
		else if (parsedText.equals("MINUTE")) {
			token.id = TKN_MINUTE;
		}
		else if (parsedText.equals("SECOND")) {
			token.id = TKN_SECOND;
		}
		else if (parsedText.equals("WEEK")) {
			token.id = TKN_WEEK;
		}
		else if (parsedText.equals("SIGN")) {
			token.id = TKN_SIGN;
		}
		else if (parsedText.equals("RND")) {
			token.id = TKN_RND;
		}
		else {
			token.id = TKN_VAR;
			token.variable = parsedText;
			token.values = dataProcessor.getValues(token.variable);
		}
		return token;
	}

	double[] calculateValues() throws RrdException {
		for (int slot = 0; slot < timestamps.length; slot++) {
			resetStack();
			for (Token token : tokens) {
				double x1, x2, x3;
				switch (token.id) {
					case TKN_NUM:
						push(token.number);
						break;
					case TKN_VAR:
						push(token.values[slot]);
						break;
					case TKN_PLUS:
						push(pop() + pop());
						break;
					case TKN_MINUS:
						x2 = pop();
						x1 = pop();
						push(x1 - x2);
						break;
					case TKN_MULT:
						push(pop() * pop());
						break;
					case TKN_DIV:
						x2 = pop();
						x1 = pop();
						push(x1 / x2);
						break;
					case TKN_MOD:
						x2 = pop();
						x1 = pop();
						push(x1 % x2);
						break;
					case TKN_SIN:
						push(Math.sin(pop()));
						break;
					case TKN_COS:
						push(Math.cos(pop()));
						break;
					case TKN_LOG:
						push(Math.log(pop()));
						break;
					case TKN_EXP:
						push(Math.exp(pop()));
						break;
					case TKN_FLOOR:
						push(Math.floor(pop()));
						break;
					case TKN_CEIL:
						push(Math.ceil(pop()));
						break;
					case TKN_ROUND:
						push(Math.round(pop()));
						break;
					case TKN_POW:
						x2 = pop();
						x1 = pop();
						push(Math.pow(x1, x2));
						break;
					case TKN_ABS:
						push(Math.abs(pop()));
						break;
					case TKN_SQRT:
						push(Math.sqrt(pop()));
						break;
					case TKN_RANDOM:
						push(Math.random());
						break;
					case TKN_LT:
						x2 = pop();
						x1 = pop();
						push(x1 < x2 ? 1 : 0);
						break;
					case TKN_LE:
						x2 = pop();
						x1 = pop();
						push(x1 <= x2 ? 1 : 0);
						break;
					case TKN_GT:
						x2 = pop();
						x1 = pop();
						push(x1 > x2 ? 1 : 0);
						break;
					case TKN_GE:
						x2 = pop();
						x1 = pop();
						push(x1 >= x2 ? 1 : 0);
						break;
					case TKN_EQ:
						x2 = pop();
						x1 = pop();
						push(x1 == x2 ? 1 : 0);
						break;
					case TKN_IF:
						x3 = pop();
						x2 = pop();
						x1 = pop();
						push(x1 != 0 ? x2 : x3);
						break;
					case TKN_MIN:
						push(Math.min(pop(), pop()));
						break;
					case TKN_MAX:
						push(Math.max(pop(), pop()));
						break;
					case TKN_LIMIT:
						x3 = pop();
						x2 = pop();
						x1 = pop();
						push(x1 < x2 || x1 > x3 ? Double.NaN : x1);
						break;
					case TKN_DUP:
						push(peek());
						break;
					case TKN_EXC:
						x2 = pop();
						x1 = pop();
						push(x2);
						push(x1);
						break;
					case TKN_POP:
						pop();
						break;
					case TKN_UN:
						push(Double.isNaN(pop()) ? 1 : 0);
						break;
					case TKN_UNKN:
						push(Double.NaN);
						break;
					case TKN_NOW:
						push(Util.getTime());
						break;
					case TKN_TIME:
						push((long) Math.round(timestamps[slot]));
						break;
					case TKN_PI:
						push(Math.PI);
						break;
					case TKN_E:
						push(Math.E);
						break;
					case TKN_AND:
						x2 = pop();
						x1 = pop();
						push((x1 != 0 && x2 != 0) ? 1 : 0);
						break;
					case TKN_OR:
						x2 = pop();
						x1 = pop();
						push((x1 != 0 || x2 != 0) ? 1 : 0);
						break;
					case TKN_XOR:
						x2 = pop();
						x1 = pop();
						push(((x1 != 0 && x2 == 0) || (x1 == 0 && x2 != 0)) ? 1 : 0);
						break;
					case TKN_PREV:
						push((slot == 0) ? Double.NaN : token.values[slot - 1]);
						break;
					case TKN_INF:
						push(Double.POSITIVE_INFINITY);
						break;
					case TKN_NEGINF:
						push(Double.NEGATIVE_INFINITY);
						break;
					case TKN_STEP:
						push(timeStep);
						break;
					case TKN_YEAR:
						push(getCalendarField(pop(), Calendar.YEAR));
						break;
					case TKN_MONTH:
						push(getCalendarField(pop(), Calendar.MONTH));
						break;
					case TKN_DATE:
						push(getCalendarField(pop(), Calendar.DAY_OF_MONTH));
						break;
					case TKN_HOUR:
						push(getCalendarField(pop(), Calendar.HOUR_OF_DAY));
						break;
					case TKN_MINUTE:
						push(getCalendarField(pop(), Calendar.MINUTE));
						break;
					case TKN_SECOND:
						push(getCalendarField(pop(), Calendar.SECOND));
						break;
					case TKN_WEEK:
						push(getCalendarField(pop(), Calendar.WEEK_OF_YEAR));
						break;
					case TKN_SIGN:
						x1 = pop();
						push(Double.isNaN(x1) ? Double.NaN : x1 > 0 ? +1 : x1 < 0 ? -1 : 0);
						break;
					case TKN_RND:
						push(Math.floor(pop() * Math.random()));
						break;
					default:
						throw new RrdException("Unexpected RPN token encountered, token.id=" + token.id);
				}
			}
			calculatedValues[slot] = pop();
			// check if stack is empty only on the first try
			if (slot == 0 && !isStackEmpty()) {
				throw new RrdException("Stack not empty at the end of calculation. " +
						"Probably bad RPN expression [" + rpnExpression + "]");
			}
		}
		return calculatedValues;
	}

	private double getCalendarField(double timestamp, int field) {
		Calendar calendar = Util.getCalendar((long) (timestamp * 1000));
		return calendar.get(field);
	}

	private void push(double x) throws RrdException {
		stack.push(x);
	}

	private double pop() throws RrdException {
		return stack.pop();
	}

	private double peek() throws RrdException {
		return stack.peek();
	}

	private void resetStack() {
		stack.reset();
	}

	private boolean isStackEmpty() {
		return stack.isEmpty();
	}

	private static final class RpnStack {
		private static final int MAX_STACK_SIZE = 1000;
		private double[] stack = new double[MAX_STACK_SIZE];
		private int pos = 0;

		void push(final double x) throws RrdException {
			if (pos >= MAX_STACK_SIZE) {
				throw new RrdException("PUSH failed, RPN stack full [" + MAX_STACK_SIZE + "]");
			}
			stack[pos++] = x;
		}

		double pop() throws RrdException {
			if (pos <= 0) {
				throw new RrdException("POP failed, RPN stack is empty ");
			}
			return stack[--pos];
		}

		double peek() throws RrdException {
			if (pos <= 0) {
				throw new RrdException("PEEK failed, RPN stack is empty ");
			}
			return stack[pos - 1];
		}

		void reset() {
			pos = 0;
		}

		boolean isEmpty() {
			return pos <= 0;
		}
	}

	private static final class Token {
		byte id = -1;
		double number = Double.NaN;
		String variable = null;
		double[] values = null;
	}
}
