/*******************************************************************************
 * 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
 *******************************************************************************/

/*
 * Java port of Tobi's original parsetime.c routine
 */
package org.jrobin.core.timespec;

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

/**
 * Class which parses at-style time specification (described in detail on the rrdfetch man page),
 * used in all RRDTool commands. This code is in most parts just a java port of Tobi's parsetime.c
 * code.
 */
public class TimeParser {
	private static final int PREVIOUS_OP = -1;

	TimeToken token;
	TimeScanner scanner;
	TimeSpec spec;

	int op = TimeToken.PLUS;
	int prev_multiplier = -1;

	/**
	 * Constructs TimeParser instance from the given input string.
	 *
	 * @param dateString at-style time specification (read rrdfetch man page
	 *                   for the complete explanation)
	 */
	public TimeParser(String dateString) {
		scanner = new TimeScanner(dateString);
		spec = new TimeSpec(dateString);
	}

	private void expectToken(int desired, String errorMessage) throws RrdException {
		token = scanner.nextToken();
		if (token.id != desired) {
			throw new RrdException(errorMessage);
		}
	}

	private void plusMinus(int doop) throws RrdException {
		if (doop >= 0) {
			op = doop;
			expectToken(TimeToken.NUMBER, "There should be number after " +
					(op == TimeToken.PLUS ? '+' : '-'));
			prev_multiplier = -1; /* reset months-minutes guessing mechanics */
		}
		int delta = Integer.parseInt(token.value);
		token = scanner.nextToken();
		if (token.id == TimeToken.MONTHS_MINUTES) {
			/* hard job to guess what does that -5m means: -5mon or -5min? */
			switch (prev_multiplier) {
				case TimeToken.DAYS:
				case TimeToken.WEEKS:
				case TimeToken.MONTHS:
				case TimeToken.YEARS:
					token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
					break;
				case TimeToken.SECONDS:
				case TimeToken.MINUTES:
				case TimeToken.HOURS:
					token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
					break;
				default:
					if (delta < 6) {
						token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
					}
					else {
						token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
					}
			}
		}
		prev_multiplier = token.id;
		delta *= (op == TimeToken.PLUS) ? +1 : -1;
		switch (token.id) {
			case TimeToken.YEARS:
				spec.dyear += delta;
				break;
			case TimeToken.MONTHS:
				spec.dmonth += delta;
				break;
			case TimeToken.WEEKS:
				delta *= 7;
				spec.dday += delta;
				break;
			case TimeToken.DAYS:
				spec.dday += delta;
				break;
			case TimeToken.HOURS:
				spec.dhour += delta;
				break;
			case TimeToken.MINUTES:
				spec.dmin += delta;
				break;
			case TimeToken.SECONDS:
			default: // default is 'seconds'
				spec.dsec += delta;
				break;
		}
		// unreachable statement
		// throw new RrdException("Well-known time unit expected after " + delta);
	}

	/**
	 * Try and read a "timeofday" specification.  This method will be called
	 * when we see a plain number at the start of a time, which means we could be
	 * reading a time, or a day.  If it turns out to be a date, then this method restores
	 * the scanner state to what it was at entry, and returns without setting anything.
	 * @throws RrdException
	 */
	private void timeOfDay() throws RrdException {
		int hour, minute = 0;
		/* save token status in case we must abort */
		scanner.saveState();
		/* first pick out the time of day - we assume a HH (COLON|DOT) MM time */
		if (token.value.length() > 2) {
			//Definitely not an hour specification; probably a date or something.  Give up now
			return;
		}
		hour = Integer.parseInt(token.value);
		token = scanner.nextToken();
		if (token.id == TimeToken.SLASH) {
			/* guess we are looking at a date */
			token = scanner.restoreState();
			return;
		}
		if (token.id == TimeToken.COLON || token.id == TimeToken.DOT) {
			expectToken(TimeToken.NUMBER, "Parsing HH:MM or HH.MM syntax, expecting MM as number, got none");
			minute = Integer.parseInt(token.value);
			if (minute > 59) {
				throw new RrdException("Parsing HH:MM or HH.MM syntax, got MM = " +
						minute + " (>59!)");
			}
			token = scanner.nextToken();
			if(token.id == TimeToken.DOT) {
				//Oh look, another dot; must have actually been a date in DD.MM.YYYY format.  Give up and return
				token = scanner.restoreState();
				return;
			}
			
		}
		/* check if an AM or PM specifier was given */
		if (token.id == TimeToken.AM || token.id == TimeToken.PM) {
			if (hour > 12) {
				throw new RrdException("There cannot be more than 12 AM or PM hours");
			}
			if (token.id == TimeToken.PM) {
				if (hour != 12) {
					/* 12:xx PM is 12:xx, not 24:xx */
					hour += 12;
				}
			}
			else {
				if (hour == 12) {
					/* 12:xx AM is 00:xx, not 12:xx */
					hour = 0;
				}
			}
			token = scanner.nextToken();
		}
		else if (hour > 23) {
			/* guess it was not a time then, probably a date ... */
			token = scanner.restoreState();
			return;
		}
		
		spec.hour = hour;
		spec.min = minute;
		spec.sec = 0;
		if (spec.hour == 24) {
			spec.hour = 0;
			spec.day++;
		}
	}

	private void assignDate(long mday, long mon, long year) throws RrdException {
		if (year > 138) {
			if (year > 1970) {
				year -= 1900;
			}
			else {
				throw new RrdException("Invalid year " + year +
						" (should be either 00-99 or >1900)");
			}
		}
		else if (year >= 0 && year < 38) {
			year += 100;		 /* Allow year 2000-2037 to be specified as   */
		}						 /* 00-37 until the problem of 2038 year will */
		/* arise for unices with 32-bit time_t     */
		if (year < 70) {
			throw new RrdException("Won't handle dates before epoch (01/01/1970), sorry");
		}
		spec.year = (int) year;
		spec.month = (int) mon;
		spec.day = (int) mday;
	}

	private void day() throws RrdException {
		long mday = 0, wday, mon, year = spec.year;
		switch (token.id) {
			case TimeToken.YESTERDAY:
				spec.day--;
				token = scanner.nextToken();
				break;
			case TimeToken.TODAY:	/* force ourselves to stay in today - no further processing */
				token = scanner.nextToken();
				break;
			case TimeToken.TOMORROW:
				spec.day++;
				token = scanner.nextToken();
				break;
			case TimeToken.JAN:
			case TimeToken.FEB:
			case TimeToken.MAR:
			case TimeToken.APR:
			case TimeToken.MAY:
			case TimeToken.JUN:
			case TimeToken.JUL:
			case TimeToken.AUG:
			case TimeToken.SEP:
			case TimeToken.OCT:
			case TimeToken.NOV:
			case TimeToken.DEC:
				/* do month mday [year] */
				mon = (token.id - TimeToken.JAN);
				expectToken(TimeToken.NUMBER, "the day of the month should follow month name");
				mday = Long.parseLong(token.value);
				token = scanner.nextToken();
				if (token.id == TimeToken.NUMBER) {
					year = Long.parseLong(token.value);
					token = scanner.nextToken();
				}
				else {
					year = spec.year;
				}
				assignDate(mday, mon, year);
				break;
			case TimeToken.SUN:
			case TimeToken.MON:
			case TimeToken.TUE:
			case TimeToken.WED:
			case TimeToken.THU:
			case TimeToken.FRI:
			case TimeToken.SAT:
				/* do a particular day of the week */
				wday = (token.id - TimeToken.SUN);
				spec.day += (wday - spec.wday);
				token = scanner.nextToken();
				break;
			case TimeToken.NUMBER:
				/* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY */
				// int tlen = token.value.length();
				mon = Long.parseLong(token.value);
				if (mon > 10L * 365L * 24L * 60L * 60L) {
					spec.localtime(mon);
					token = scanner.nextToken();
					break;
				}
				if (mon > 19700101 && mon < 24000101) { /*works between 1900 and 2400 */
					year = mon / 10000;
					mday = mon % 100;
					mon = (mon / 100) % 100;
					token = scanner.nextToken();
				}
				else {
					token = scanner.nextToken();
					if (mon <= 31 && (token.id == TimeToken.SLASH || token.id == TimeToken.DOT)) {
						int sep = token.id;
						expectToken(TimeToken.NUMBER, "there should be " +
								(sep == TimeToken.DOT ? "month" : "day") +
								" number after " +
								(sep == TimeToken.DOT ? '.' : '/'));
						mday = Long.parseLong(token.value);
						token = scanner.nextToken();
						if (token.id == sep) {
							expectToken(TimeToken.NUMBER, "there should be year number after " +
									(sep == TimeToken.DOT ? '.' : '/'));
							year = Long.parseLong(token.value);
							token = scanner.nextToken();
						}
						/* flip months and days for European timing */
						if (sep == TimeToken.DOT) {
							long x = mday;
							mday = mon;
							mon = x;
						}
					}
				}
				mon--;
				if (mon < 0 || mon > 11) {
					throw new RrdException("Did you really mean month " + (mon + 1));
				}
				if (mday < 1 || mday > 31) {
					throw new RrdException("I'm afraid that " + mday +
							" is not a valid day of the month");
				}
				assignDate(mday, mon, year);
				break;
		}
	}

	/**
	 * Parses the input string specified in the constructor.
	 *
	 * @return Object representing parsed date/time.
	 * @throws RrdException Thrown if the date string cannot be parsed.
	 */
	public TimeSpec parse() throws RrdException {
		long now = Util.getTime();
		int hr = 0;
		/* this MUST be initialized to zero for midnight/noon/teatime */
		/* establish the default time reference */
		spec.localtime(now);
		token = scanner.nextToken();
		switch (token.id) {
			case TimeToken.PLUS:
			case TimeToken.MINUS:
				break; /* jump to OFFSET-SPEC part */
			case TimeToken.START:
				spec.type = TimeSpec.TYPE_START;
				/* FALLTHRU */
			case TimeToken.END:
				if (spec.type != TimeSpec.TYPE_START) {
					spec.type = TimeSpec.TYPE_END;
				}
				spec.year = spec.month = spec.day = spec.hour = spec.min = spec.sec = 0;
				/* FALLTHRU */
			case TimeToken.NOW:
				int time_reference = token.id;
				token = scanner.nextToken();
				if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
					break;
				}
				if (time_reference != TimeToken.NOW) {
					throw new RrdException("Words 'start' or 'end' MUST be followed by +|- offset");
				}
				else if (token.id != TimeToken.EOF) {
					throw new RrdException("If 'now' is followed by a token it must be +|- offset");
				}
				break;
				/* Only absolute time specifications below */
			case TimeToken.NUMBER:
				timeOfDay();
				//Keep going; there might be a date after the time of day, which day() will pick up
				/* fix month parsing */
			case TimeToken.JAN:
			case TimeToken.FEB:
			case TimeToken.MAR:
			case TimeToken.APR:
			case TimeToken.MAY:
			case TimeToken.JUN:
			case TimeToken.JUL:
			case TimeToken.AUG:
			case TimeToken.SEP:
			case TimeToken.OCT:
			case TimeToken.NOV:
			case TimeToken.DEC:
			case TimeToken.TODAY:
			case TimeToken.YESTERDAY:
			case TimeToken.TOMORROW:
				day();
				if (token.id != TimeToken.NUMBER) {
					break;
				}
				//Allows (but does not require) the time to be specified after the day.  This extends the rrdfetch specifiation
				timeOfDay();
				break;

				/* evil coding for TEATIME|NOON|MIDNIGHT - we've initialized
				 * hr to zero up above, then fall into this case in such a
				 * way so we add +12 +4 hours to it for teatime, +12 hours
				 * to it for noon, and nothing at all for midnight, then
				 * set our rettime to that hour before leaping into the
				 * month scanner
				 */
			case TimeToken.TEATIME:
				hr += 4;
				/* FALLTHRU */
			case TimeToken.NOON:
				hr += 12;
				/* FALLTHRU */
			case TimeToken.MIDNIGHT:
				spec.hour = hr;
				spec.min = 0;
				spec.sec = 0;
				token = scanner.nextToken();
				day();
				break;
			default:
				throw new RrdException("Unparsable time: " + token.value);
		}

		/*
		 * the OFFSET-SPEC part
		 *
		 * (NOTE, the sc_tokid was prefetched for us by the previous code)
		 */
		if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
			scanner.setContext(false);
			while (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS ||
					token.id == TimeToken.NUMBER) {
				if (token.id == TimeToken.NUMBER) {
					plusMinus(PREVIOUS_OP);
				}
				else {
					plusMinus(token.id);
				}
				token = scanner.nextToken();
				/* We will get EOF eventually but that's OK, since
				token() will return us as many EOFs as needed */
			}
		}
		/* now we should be at EOF */
		if (token.id != TimeToken.EOF) {
			throw new RrdException("Unparsable trailing text: " + token.value);
		}
		return spec;
	}
}