package org.inria.bmajwatcher.server.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * Parses a quartz cron expression into a list of dates.
 * See syntax here : http://www.quartz-scheduler.org/docs/tutorials/crontrigger.html
 * 
 * @author rsabas
 *
 */
public class CronParser {
	
	public static int MONTHLY = 0;
	
	public static void main(String[] args) {
//		getDates("0 15 10 15 * ?");	// Fire at 10:15am on the 15th day of every month
//		getDates("0 15 10 ? * MON-FRI"); // Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday
//		getDates("0 11 11 11 11 ?"); // Fire every November 11th at 11:11am.
//		getDates("0 15 10 ? * 6L 2002-2005"); // Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005
//		getDates("0 0 12 1/5 * ?"); // Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
//		getDates("0 15 10 ? * 6#3"); // Fire at 10:15am on the third Friday of every month
//		getDates("0 15 10 L * ?"); // Fire at 10:15am on the last day of every month
//		getDates("0 15 10 * * ? 2005"); // Fire at 10:15am every day during the year 2005
//		getDates("0 10,44 14 ? 3 WED"); // Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.
//		getDates("0 0-5 14 * * ?"); // Fire every minute starting at 2pm and ending at 2:05pm, every day
		
//		getDates("0 0-5 14 * * ?", new Date(), MONTHLY);
	}

	/**
	 * Returns a list of dates within the given range according
	 * to the given date. e.g if range is 7, returns the dates for
	 * the next 7 days.
	 * If range is 0, return for the whole month.
	 * 
	 * We asume that the expressions are valid and that
	 * each field contains only one type of special character.
	 * 
	 * @param expr
	 * @param current
	 * @param range
	 * @return
	 */
	public static List<Date> getDates(String expr, Date current, int range) {

		String[] elts = expr.trim().split("\\s+");
		List<Integer> years;
		
		if (elts.length == 7) { // Year is specified
			years = getYears(elts[6]);
		} else {
			years = new ArrayList<Integer>();
			Calendar cal = Calendar.getInstance();
			cal.setTime(current);
			years.add(cal.get(Calendar.YEAR));
		}
		
		List<Integer> minutes = getMinutes(elts[1]);
		List<Integer> hours = getHours(elts[2]);
		List<Integer> months = getMonths(elts[4]);
		boolean isDayOfMonth = elts[5].equals("?");
		List<Date> dates = new ArrayList<Date>();
		
		for (int year : years) {
			for (int month : months) {
				List<Integer> days;
				if (isDayOfMonth)
					days = getDays(elts[3], isDayOfMonth, year, month);
				else
					days = getDays(elts[5], isDayOfMonth, year, month);
				for (int day : days) {
					for (int hour : hours) {
						for (int minute : minutes) {
							String s = hour + ":" + minute + " " + day + "/" + month + "/" + year;
							SimpleDateFormat sdf = new SimpleDateFormat("HH:mm dd/MM/yyyy");
							Date d = null;
							try {
								d = sdf.parse(s);
							} catch (ParseException e) {
								e.printStackTrace();
							}
							if (range == MONTHLY && isInMonth(current, d))
								dates.add(d);
							else if (isInRange(current, d, range))
								dates.add(d);
						}
					}
				}
			}
		}
		
		Collections.sort(dates);
		
		return dates;
	}
	
	private static boolean isInRange(Date ref, Date toCompare, int dayCount) {
		Calendar calStart = Calendar.getInstance();
		calStart.setTime(ref);
		calStart.set(Calendar.HOUR_OF_DAY, 0);
		calStart.set(Calendar.MINUTE, 0);
		calStart.set(Calendar.SECOND, 0);
		
		Calendar calEnd = Calendar.getInstance();
		calEnd.setTime(calStart.getTime());
		calEnd.add(Calendar.DAY_OF_YEAR, dayCount);
		
		Calendar cmp = Calendar.getInstance();
		cmp.setTime(toCompare);
		
		return (cmp.after(calStart) && cmp.before(calEnd));
	}
	
	/*
	private static boolean isInWeek(Date ref, Date toCompare) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(ref);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		int firstDayOfWeek = cal.getFirstDayOfWeek();
		cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);
		Calendar cmp = Calendar.getInstance();
		cmp.setTime(toCompare);
		
		if (cmp.compareTo(cal) < 0)
			return false;
		
		if (firstDayOfWeek == Calendar.MONDAY) {
			cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
		} else
			cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		
		return (cmp.compareTo(cal) <= 0);
	}*/
	
	private static boolean isInMonth(Date ref, Date toCompare) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(ref);
		cal.set(Calendar.DAY_OF_MONTH, 1);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		
		Calendar cmp = Calendar.getInstance();
		cmp.setTime(toCompare);
		
		if (cmp.compareTo(cal) < 0)
			return false;
		
		cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		
		return (cmp.compareTo(cal) <= 0);
	}
	
	private static List<Integer> getUnits(String expr, int start, int max) {
		List<Integer> units = new ArrayList<Integer>();
		if (expr.contains(",")) {
			String[] elts = expr.split(",");
			for (String unit : elts) {
				int value = Integer.valueOf(unit); 
				if (value <= max)
					units.add(value);
			}
		} else if (expr.contains("-")) {
			String[] elts = expr.split("-");
			for (int i = Integer.valueOf(elts[0]); i <= Integer.valueOf(elts[1]) && i <= max; i++)
				units.add(i);
		} else if (expr.contains("/")) {
			String[] elts = expr.split("/");
			int _start = Integer.valueOf(elts[0]);
			for (int i = _start; i <= max; i += Integer.valueOf(elts[1]))
				units.add(i);
		} else if (expr.equals("*")) {
			for (int i = start; i <= max; i++)
				units.add(i);
		} else { // A numeric value
			int unit = Integer.valueOf(expr);
			if (unit <= max)
				units.add(unit);
		}
		
		return units;
	}
	
	private static List<Integer> getMinutes(String expr) {
		return getUnits(expr, 0, 59);
	}
	
	private static List<Integer> getHours(String expr) {
		return getUnits(expr, 0, 23);
	}
	
	private static List<Integer> getDays(String expr, boolean isDayOfMonth, int year, int month) {
		List<Integer> days = new ArrayList<Integer>();
		Calendar calendar = Calendar.getInstance();
		calendar.set(Calendar.YEAR, year);
		calendar.set(Calendar.MONTH, month - 1); // starts from zero
		int numberOfDaysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
		
		if (isDayOfMonth) {
			if (expr.equals("LW")) { // Last weekday of month
				int currentDay = numberOfDaysInMonth;
				calendar.set(Calendar.DAY_OF_MONTH, currentDay);
				while (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY 
						|| calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
					calendar.set(Calendar.DAY_OF_MONTH, --currentDay);
				}
				days.add(currentDay);
			} else if (expr.equals("L")) { // last day of the month
				days.add(numberOfDaysInMonth);
			} else if (expr.contains("W")) { // <number>W : closest weekday to number
				int day = Integer.valueOf(expr.substring(0, expr.indexOf('W')));
				calendar.set(Calendar.DAY_OF_MONTH, day);
				if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
					if (day != 1) // We dont go a month backward
						days.add(day - 1); // Go on friday
					else // Next monday
						days.add(day + 2);
				} else if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) {
					if (day != numberOfDaysInMonth) // Dont go to the next month
						days.add(day + 1);
					else // Friday
						days.add(day - 2);
				} else // A week day
					days.add(day);
			} else
				days = getUnits(expr, 1, numberOfDaysInMonth);
		} else { // Day of week
			if (expr.contains("L")) {
				if (expr.length() == 1) // Last day of week
					days.addAll(convertDayOfWeekIntoDaysOfMonth(7, calendar));
				else { // Last x of month
					int day = Integer.valueOf(expr.substring(0, expr.length() - 1));
					int currentDay = numberOfDaysInMonth;
					calendar.set(Calendar.DAY_OF_MONTH, currentDay);
					while (calendar.get(Calendar.DAY_OF_WEEK) != day) {
						calendar.set(Calendar.DAY_OF_MONTH, --currentDay);
					}
					days.add(currentDay);
				}
			} else if (expr.contains("#")) {
				int number = Integer.valueOf(String.valueOf(expr.charAt(2)));
				int day = Integer.valueOf(String.valueOf(expr.charAt(0)));
				int pos = 1;
				
				for (int i = 1; i <= numberOfDaysInMonth; i++) {
					calendar.set(Calendar.DAY_OF_MONTH, i);
					if (calendar.get(Calendar.DAY_OF_WEEK) == day) {
						if (pos == number) {
							days.add(i);
							break;
						} else
							pos++;
					}
				}
				
			} else {
				List<Integer> weekDays = getUnits(getDayOfWeekNumber(expr), 1, 7);
				for (Integer i : weekDays)
					days.addAll(convertDayOfWeekIntoDaysOfMonth(i, calendar));
			}
		}
		
		return days;
	}
	
	private static List<Integer> convertDayOfWeekIntoDaysOfMonth(int dayOfWeek, Calendar calendar) {
		List<Integer> days = new ArrayList<Integer>();
		/*
		 * First week
		 */
		calendar.set(Calendar.DAY_OF_MONTH, 1);
		int firstWeek = calendar.get(Calendar.WEEK_OF_MONTH);
		
		if (compareDays(calendar.get(Calendar.DAY_OF_WEEK), dayOfWeek) <= 0) {
			calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
			days.add(calendar.get(Calendar.DAY_OF_MONTH));
		}
		
		int weeksInMonth = calendar.getActualMaximum(Calendar.WEEK_OF_MONTH);
		for (int i = firstWeek + 1; i < weeksInMonth; i++) {
			calendar.set(Calendar.WEEK_OF_MONTH, i);
			calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
			days.add(calendar.get(Calendar.DAY_OF_MONTH));
		}
		
		/*
		 * Last week
		 */
		calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
		if (compareDays(calendar.get(Calendar.DAY_OF_WEEK), dayOfWeek) >= 0) {
			calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
			days.add(calendar.get(Calendar.DAY_OF_MONTH));
		}
		
		return days;
	}
	
	private static int compareDays(int day1, int day2) {
		Calendar cal = Calendar.getInstance();
		if (day1 == day2)
			return 0;
		
		if (day1 == Calendar.SUNDAY && cal.getFirstDayOfWeek() == Calendar.SUNDAY)
			return -1;
		
		if (day1 == Calendar.SUNDAY && cal.getFirstDayOfWeek() == Calendar.MONDAY)
			return 1;
		
		if (day2 == Calendar.SUNDAY && cal.getFirstDayOfWeek() == Calendar.SUNDAY)
			return 1;
		
		if (day2 == Calendar.SUNDAY && cal.getFirstDayOfWeek() == Calendar.MONDAY)
			return -1;
		
		return day1 - day2;
		
	}
	
	private static List<Integer> getMonths(String expr) {
		return getUnits(getMonthNumber(expr), 1, 12);
	}
	
	private static List<Integer> getYears(String expr) {
		return getUnits(expr, 1970, 2099);
	}
	
	private static String getMonthNumber(String expr) {
		return expr.replace("JAN", "1").replace("FEB", "2").replace("MAR", "3")
				.replace("APR", "4").replace("MAY", "5").replace("JUN", "6")
				.replace("JUL", "7").replace("AUG", "8").replace("SEP", "9")
				.replace("OCT", "10").replace("NOV", "11").replace("DEC", "12");
	}
	
	private static String getDayOfWeekNumber(String expr) {
		return expr.replace("SUN", "1").replace("MON", "2").replace("TUE", "3")
				.replace("WED", "4").replace("THU", "5").replace("FRI", "6").replace("SAT", "7");
	}
	
	

}
