/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.scheduler.common;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

import org.joda.time.DateTime;

import divconq.scheduler.limit.CheckInfo;
import divconq.scheduler.limit.MonthdayKind;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;
import divconq.xml.XElement;

public class MonthHelper extends CommonHelper {
	protected List<MonthEntry> months = new ArrayList<MonthEntry>();
	
	// first time this code was run since the module/server was started
	// requires special check for any missed runs
	protected boolean firstCall = true;
	
	@Override
	public void init(CommonSchedule schedule, XElement config) {
		if (config != null) {
			for (XElement el : config.selectAll("Months")) {
				MonthEntry entry = new MonthEntry();
				entry.init(el);
				this.months.add(entry);
			}
		}
		
		if (this.months.size() == 0) {
			MonthEntry entry = new MonthEntry();
			entry.init(null);
			this.months.add(entry);
		}
	}
	
	// return null to indicate this can never run again
	@Override
	public DateTime next() {
		if (this.firstCall) {
			this.firstCall = false;
			
			if (this.limits.isEnded())
				return null;
			
			// if the valid range for schedule has not started yet, then 
			// schedule way out
			if (!this.limits.isStarted()) {
				this.firstCall = false;
				this.last = new DateTime(this.limits.getFirstDate());
			}
			// if there was no past runs, do not try to find them
			// or if there are not any required
			//else if (this.last == null) {
			//	this.last = new DateTime();		// start with today (now)
			//}
			// see if we need to run because of a missed past schedule
			else {
				if (this.last == null) 
					this.last = new DateTime(1970, 1, 1, 0, 0, 0, 0);		// start with deep past
					
				for (MonthEntry we : this.months) {
					if (we.checkStartupNeedsRun()) {		
						System.out.println("run asap!!!!");
						this.last = this.limits.nextAllowedRun();
						return this.last;
					}
				}

				if (this.last == null)
					this.last = new DateTime();		// start with today (now)
			}
		}
		
		// we should never get here with a really out of date "last", today is fine
		CheckInfo ci = new CheckInfo();
		ci.setWhen(this.last);

		while (true) {
			for (MonthEntry we : this.months) {
				MonthdayEntry me = we.getApplicable(ci);
				
				if (me != null) {
					ScheduleEntry se = me.dailySchedule.next(ci.getWhen());
					
					while (se != null) {
						// add the scheduled entry to the time
						// (first time with a given date, this is the time past midnight)
						DateTime tlast = TimeUtil.withTime(ci.getWhen(), se.getTime());
						
						// we cannot schedule again, the schedule is expired
						if (this.limits.isEnded(tlast)) {
							this.last = null;
							return null;
						}
						
						// can we run at the suggested time?
						if (this.limits.checkForRun(tlast)) {
							this.last = tlast;
							return this.last;
						}
						
						// if not, should we run asap after the suggested time
						if (se.isRunIfMissed()) {
							this.last = this.limits.nextAllowedRunAfter(tlast);
							return this.last;
						}
						
						// go on and check the next time
						se = me.dailySchedule.next(tlast);
					}
					
					break;
				}
			}
			
			ci.incrementDay();
		}
	}

	class MonthEntry implements IDateChecker {
		protected List<MonthdayEntry> monthly = new ArrayList<MonthdayEntry>();
		// (0 = jan, 11 = dec)
		protected BitSet monthOfYear = new BitSet(12);
		
		public void init(XElement config) {
			if (config != null) {
				if ("True".equals(config.getAttribute("All")))
					this.monthOfYear.set(0, 11);
				
				if ("True".equals(config.getAttribute("January")))
					this.monthOfYear.set(0);
				
				if ("True".equals(config.getAttribute("February")))
					this.monthOfYear.set(1);
				
				if ("True".equals(config.getAttribute("March")))
					this.monthOfYear.set(2);
				
				if ("True".equals(config.getAttribute("April")))
					this.monthOfYear.set(3);
				
				if ("True".equals(config.getAttribute("May")))
					this.monthOfYear.set(4);
				
				if ("True".equals(config.getAttribute("June")))
					this.monthOfYear.set(5);
				
				if ("True".equals(config.getAttribute("July")))
					this.monthOfYear.set(6);
				
				if ("True".equals(config.getAttribute("August")))
					this.monthOfYear.set(7);
				
				if ("True".equals(config.getAttribute("September")))
					this.monthOfYear.set(8);
				
				if ("True".equals(config.getAttribute("October")))
					this.monthOfYear.set(9);
				
				if ("True".equals(config.getAttribute("November")))
					this.monthOfYear.set(10);
				
				if ("True".equals(config.getAttribute("December")))
					this.monthOfYear.set(11);
				
				String[] slots = { "Monthday", "First", "Second", "Third", "Fourth", "Last" };
				
				for (String slot : slots)			
					for (XElement el : config.selectAll(slot)) {
						MonthdayEntry ww = new MonthdayEntry();
						ww.init(el);
						this.monthly.add(ww);
					}
			}
			
			// if none set, then default to all
			if (monthOfYear.cardinality() == 0)
				this.monthOfYear.set(0, 11);
			
			if (this.monthly.size() == 0) { 
				MonthdayEntry ww = new MonthdayEntry();
				ww.init(null);
				this.monthly.add(ww);
			}
		}		

		// the meaning of this method is off a little from expected
		// normally this should only check monthOfYear and ignore the monthdays
		// but for the checkStartupNeedsRun this needs to check the monthdays
		// lets see if any problem come up
		@Override
		public boolean checkDate(CheckInfo ci) {
			if (this.monthOfYear.get(ci.getMonthOfYear() - 1)) 
				for (MonthdayEntry w : this.monthly) 
					if (w.checkDate(ci))
						return true;
			
			return false;
		}

		public MonthdayEntry getApplicable(CheckInfo ci) {
			if (this.monthOfYear.get(ci.getMonthOfYear() - 1)) 
				for (MonthdayEntry w : this.monthly) 
					if (w.checkDate(ci))
						return w;
			
			return null;
		}
		
		public boolean checkStartupNeedsRun() {
			for (MonthdayEntry w : this.monthly) 
				if (MonthHelper.this.checkStartupNeedsRun(w.dailySchedule, this)) 
					return true;
			
			return false;
		}
	}
	
	class MonthdayEntry implements IDateChecker {
		ScheduleList dailySchedule = new ScheduleList();		
		// (0 = monday, 6 = sunday)
		BitSet dayOfWeek = new BitSet(7);
		
		protected MonthdayKind kind = MonthdayKind.Set;
		
		// if type SET then look here for the listed days 
		protected BitSet dayOfMonth = new BitSet(31);
		// does the set include "Last"
		protected boolean isLastDayOfMonth = false;		
		
		public void init(XElement config) {
			if (config != null) {
				String kind = config.getName();
				
				if ("First".equals(kind))
					this.kind = MonthdayKind.First;
				else if ("Second".equals(kind))
					this.kind = MonthdayKind.Second;
				else if ("Third".equals(kind))
					this.kind = MonthdayKind.Third;
				else if ("Fourth".equals(kind))
					this.kind = MonthdayKind.Fourth;
				else if ("Last".equals(kind))
					this.kind = MonthdayKind.Last;
				
				if (this.kind != MonthdayKind.Set) {
					if ("True".equals(config.getAttribute("All")))
						this.dayOfWeek.set(0, 6);
	
					if ("True".equals(config.getAttribute("Monday")))
						this.dayOfWeek.set(0);
					
					if ("True".equals(config.getAttribute("Tuesday")))
						this.dayOfWeek.set(1);
					
					if ("True".equals(config.getAttribute("Wednesday")))
						this.dayOfWeek.set(2);
					
					if ("True".equals(config.getAttribute("Thursday")))
						this.dayOfWeek.set(3);
					
					if ("True".equals(config.getAttribute("Friday")))
						this.dayOfWeek.set(4);
					
					if ("True".equals(config.getAttribute("Saturday")))
						this.dayOfWeek.set(5);
					
					if ("True".equals(config.getAttribute("Sunday")))
						this.dayOfWeek.set(6);
					
					// if none set then default to all
					if (this.dayOfWeek.cardinality() == 0)
						this.dayOfWeek.set(0, 6);
				}
				else {
					String list = config.getAttribute("List");
					
					if (StringUtil.isEmpty(list) || "All".equals(list)) {
						this.isLastDayOfMonth = true;
						this.dayOfMonth.set(0, 30);
					}
					else {
						String[] parts = list.split(",");
						
						for (String p : parts) {
							if (p.equals("Last")) {
								this.isLastDayOfMonth = true;
								break;
							}
							
							int idx = (int)StringUtil.parseInt(p, 0) - 1;
							
							if (idx > -1)
								this.dayOfMonth.set(idx);
						}
					}
				}
			}
			
			if ((this.kind == MonthdayKind.Set) && (this.dayOfMonth.cardinality() == 0)) {
				this.isLastDayOfMonth = true;
				this.dayOfMonth.set(0, 30);
			}
			
			this.dailySchedule.init(config);
		}

		@Override
		public boolean checkDate(CheckInfo si) {
			if (this.kind == MonthdayKind.Set) {
				if (si.isLastDay() && this.isLastDayOfMonth)
					return true;
				else if (this.dayOfMonth.get(si.getDayOfMonth() - 1))
					return true;
			}
			else if (this.dayOfWeek.get(si.getDayOfWeek() - 1)) { 
				if ((this.kind == MonthdayKind.First) && (si.getMonthPlacement() == 1))  
					return true;
				else if ((this.kind == MonthdayKind.Second) && (si.getMonthPlacement() == 2))  
					return true;
				else if ((this.kind == MonthdayKind.Third) && (si.getMonthPlacement() == 3))  
					return true;
				else if ((this.kind == MonthdayKind.Fourth) && (si.getMonthPlacement() == 4))  
					return true;
				else if ((this.kind == MonthdayKind.Last) && (si.isLastPlacement())) 
					return true;
			}
			
			return false;
		}
	}
}