/*
The contents of this file are subject to the Common Public Attribution License
Version 1.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.projity.com/license . The License is based on the Mozilla Public
License Version 1.1 but Sections 14 and 15 have been added to cover use of
software over a computer network and provide for limited attribution for the
Original Developer. In addition, Exhibit A has been modified to be consistent
with Exhibit B.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License. The
Original Code is OpenProj. The Original Developer is the Initial Developer and
is Projity, Inc. All portions of the code written by Projity are Copyright (c)
2006, 2007. All Rights Reserved. Contributors Projity, Inc.

Alternatively, the contents of this file may be used under the terms of the
Projity End-User License Agreeement (the Projity License), in which case the
provisions of the Projity License are applicable instead of those above. If you
wish to allow use of your version of this file only under the terms of the
Projity License and not to allow others to use your version of this file under
the CPAL, indicate your decision by deleting the provisions above and replace
them with the notice and other provisions required by the Projity  License. If
you do not delete the provisions above, a recipient may use your version of this
file under either the CPAL or the Projity License.

[NOTE: The text of this license may differ slightly from the text of the notices
in Exhibits A and B of the license at http://www.projity.com/license. You should
use the latest text at http://www.projity.com/license for your modifications.
You may not remove this license text from the source files.]

Attribution Information: Attribution Copyright Notice: Copyright � 2006, 2007
Projity, Inc. Attribution Phrase (not exceeding 10 words): Powered by OpenProj,
an open source solution from Projity. Attribution URL: http://www.projity.com
Graphic Image as provided in the Covered Code as file:  openproj_logo.png with
alternatives listed on http://www.projity.com/logo

Display of Attribution Information is required in Larger Works which are defined
in the CPAL as a work which combines Covered Code or portions thereof with code
not governed by the terms of the CPAL. However, in addition to the other notice
obligations, all copies of the Covered Code in Executable and Source Code form
distributed must, as a form of attribution of the original author, include on
each user interface screen the "OpenProj" logo visible to all users.  The
OpenProj logo should be located horizontally aligned with the menu bar and left
justified on the top left of the screen adjacent to the File menu.  The logo
must be at least 100 x 25 pixels.  When users click on the "OpenProj" logo it
must direct them back to http://www.projity.com.
*/
package com.projity.pm.calendar;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.TreeSet;

import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.pool.BasePoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

import com.projity.datatype.Duration;
import com.projity.server.access.ErrorLogger;
import com.projity.util.DateTime;

/**
 * This class holds specific calendar informatin either for a base calendar or a concrete one, as well as date math functions
 */
public class CalendarDefinition implements WorkCalendar, Cloneable {
	static final long serialVersionUID = 73883742020831L;
	TreeSet dayExceptions = null;
	WorkDay[] exceptions = null;
	WorkWeek week = new WorkWeek();
	protected long id=-1L;

	/**
	 *
	 */
	public CalendarDefinition() {
		super();
		dayExceptions = new TreeSet();
		// TODO Auto-generated constructor stub
	}

	public CalendarDefinition(CalendarDefinition base, CalendarDefinition differences) {
		if (base == null) {
			week = new WorkWeek();
		} else {
			week = (WorkWeek)  base.week.clone(); // copy the week days
		}
		week.addDaysFrom(differences.week); // Now replace any special weekdays

		dayExceptions = (TreeSet) differences.dayExceptions.clone(); // copy from differences
		if (base != null)
			dayExceptions.addAll( base.dayExceptions); // add in base days. If day is already present it will not be added
		addSentinelsAndMakeArray();

		if (!testValid())
			System.out.println("calendar is invalid " + this.getName());
	}

	public boolean testValid() {
		if (week == null)
			return false;
		for (int i = 0; i < 7; i++)
			if (week.getWeekDay(i) == null)
				return false;
		return true;

	}
	void addSentinelsAndMakeArray() {
		// Add endpoint sentinels.  This facilitates algorithms which will no longer need to check for boundary conditions
		dayExceptions.add(WorkDay.MINIMUM);
		dayExceptions.add(WorkDay.MAXIMUM);
		exceptions = new WorkDay[dayExceptions.size()];
		dayExceptions.toArray(exceptions);

	}
	public WorkDay[] getExceptions() {
		return exceptions;
	}

	public WorkDay getWeekDay(int d) {
		return week.getWeekDay(d);
	}
	void addOrReplaceException(WorkDay exceptionDay) {
		dayExceptions.remove(exceptionDay); // remove any existing
		dayExceptions.add(exceptionDay);
		exceptions = new WorkDay[dayExceptions.size()];
		dayExceptions.toArray(exceptions);
	}


	/* (non-Javadoc)
	 * @see java.lang.Object#clone()
	 */
	public Object clone() throws CloneNotSupportedException {
		CalendarDefinition newOne = (CalendarDefinition) super.clone();
		newOne.week = (WorkWeek) week.clone();
		newOne.dayExceptions = new TreeSet();

		Iterator i = dayExceptions.iterator();
		while (i.hasNext())
			newOne.dayExceptions.add(((WorkDay)i.next()).clone());
		return newOne;
	}


	/**
	 * This method adjusts the given time to a working time in the calendar.
	 * The algorithm just subtracts a tick and adds it back for sooner or vice versa for later
	 * @param date
	 * @param useSooner
	 * @return
	 */
	public long adjustInsideCalendar(long date, boolean useSooner) {
		long result;
		if (date < 0) {
			date = -date;
			useSooner = !useSooner;
		}
		if (useSooner) {
			long backOne = add(date,-MILLIS_IN_MINUTE,useSooner);
			result =add(backOne,MILLIS_IN_MINUTE,useSooner);
		} else {
			long aheadOne = add(date,MILLIS_IN_MINUTE,useSooner);
			result =add(aheadOne,-MILLIS_IN_MINUTE,useSooner);
		}
		return result;
	}



	/**
	 * Algorithm to add a duration to a date.  This code MUST be very fast as it is the most executed code in the program.
	 * The time required by the algorithm is determined by the number of exceptions encountered and not the duration itself.
	 * To handle reverse scheduling, the date can be negative.  In this case, the date is converted to a positive value, but the duration
	 * is negated.
	 */
	public long add(long date, long duration, boolean useSooner) {
		if (date == 0) // don't bother treating null dates since they will never be valid for calculations
			return 0;
		long result = date;
		boolean forward = true;
		boolean negative = date < 0;
		boolean elapsed = Duration.isElapsed(duration);
		duration = Duration.millis(duration);

		if (negative) {
			date = -date;
			duration = -duration;
			useSooner = !useSooner;
			if (duration == 0)
				forward = false;
		}




		if (elapsed) { // elapsed times do not use calendars, though the result must fall within working time
			result = adjustInsideCalendar(date + duration,useSooner);
		} else {
			if (duration < 0) {
				forward = false;
				duration = -duration;
			}
			//TODO move current day into iterator for speed
			CalendarIterator iterator = CalendarIteratorFactory.getInstance(); // use object pool for speed
			long currentDay = iterator.dayOf(date);
			iterator.initialize(this,forward,currentDay);
			WorkingHours current = iterator.getNext(currentDay);
			duration -= current.calcWorkTime(iterator.timeOf(date),forward);// handle the first day

			long numWeeks;

	/*
	 * First, do a "rough tuning" to get within a week of destination day.  This part of the algorithm will
	 * see how many weeks there are in the duration, subtract off the normal working time for a week for each week.
	 * and position the day correctly.  It then adjusts the duration based on any exception days during those weeks.
	 * It is possible, if there are many exceptions, that after adjusting for exception days, there are still weeks of
	 * work left.  That is why this is called in a loop.
	 */

//TODO This code may not working correctly
			long weekDuration = week.getDuration();
			while ((numWeeks = (duration / weekDuration)) != 0) {
				currentDay = iterator.nextDay(currentDay); // move to next day, first is done
				currentDay = iterator.moveNumberOfDays((int) (WorkWeek.DAYS_IN_WEEK * (forward ? numWeeks : -numWeeks)),currentDay);
				duration -= (numWeeks * weekDuration); // subtract off fixed duration
				duration -= iterator.exceptionDurationDifference(currentDay); // subtract off difference.

				if (duration <= 0) { // if exceptions cause too much duration, then go back in other direction
					iterator.reverseDirection();
					duration = -duration;
					forward = !forward; // todo is this necessary?
				}
				else //TODO verify that this should be in else.
					currentDay = iterator.prevDay(currentDay); // move back a day for fine tuning which adds it back

			}
//
//
	/*
	 * This part of the algorithm is the fine tuning.  It does through the remaining deays and treats them one by one.
	 * Because of the week treatment above, this is guaranteed to go through 6 days at the most.
	 */		while (duration >= 0) { // add in days until we go exactly on the spot or past it
				if (duration == 0 && (forward == useSooner))
					break;
				currentDay = iterator.nextDay(currentDay);
				current = iterator.getNext(currentDay);
				duration -= current.getDuration(); // use exception day
			}
			// Handle the last day
	 		long time = -1;
	 		while (true)  {
				if (forward) {
					time = current.calcTimeAtRemainingWork(-duration);
				}else
					time = current.calcTimeAtWork(-duration);
				if (time != -1)
					break;
				currentDay = iterator.nextDay(currentDay);
				current = iterator.getNext(currentDay);
	 		};
			result = currentDay + time;

	 		CalendarIteratorFactory.recycle(iterator); //No longer using iterator, return it to pool
		}

		// if input was negative time, return a negative value
		if (negative)
			result = -result;
		return result;
	}


	/**
	 * Get difference of two dates: laterDate - earlierDate according to calendar
	 */
	public long compare(long laterDate, long earlierDate, boolean elapsed) {
		boolean negative = laterDate < 0;
		if (negative) {
			laterDate = -laterDate;
			earlierDate = -earlierDate;
		}

		if (elapsed) { // if the desired duration is elapsed time, then just to a simple subtraction
			return laterDate - earlierDate;
		}


		// if later is before earlier swap the dates.  The value of swap is tested later and sign is reversed if it is used
		long swap = 0;
		if (laterDate < earlierDate) {
			swap = earlierDate;
			earlierDate = laterDate;
			laterDate = swap;

		}
		if (earlierDate == 0) // degenerate case.  A 0 date means undefined, so don't process it
			return laterDate;

		CalendarIterator iterator = CalendarIteratorFactory.getInstance(); // use object pool for speed
		long earlierDay = iterator.dayOf(earlierDate);
		long laterDay = iterator.dayOf(laterDate);
		long currentDay;
		iterator.initialize(this,true,earlierDay);
		WorkingHours current = iterator.getNext(earlierDay);
		long duration = 0;


		// Algo starts here
		// treat start day
		duration += current.calcWorkTimeAfter(iterator.timeOf(earlierDate));
		currentDay = iterator.nextDay(earlierDay); // move to next day, first is done

		/*
		 * First add in weeks, adjusting for exception days
		 */
		long numWeeks = (iterator.dayOf(laterDate) - currentDay) / WorkWeek.MS_IN_WEEK ;
		if (numWeeks != 0) {
			currentDay = iterator.moveNumberOfDays((int) (WorkWeek.DAYS_IN_WEEK * numWeeks),currentDay);
			duration += numWeeks * week.getDuration(); // add on normal working duration
			duration += iterator.exceptionDurationDifference(currentDay); // add difference.
		}

		// treat remaining middle days  (no more than 6) and the end day
		for (; currentDay <= laterDay; currentDay = iterator.nextDay(currentDay)) {
			current = iterator.getNext(currentDay);
			duration += current.getDuration();
		}

		// subtract out part of the end day that is later then laterDate
		duration -= current.calcWorkTimeAfter(iterator.timeOf(laterDate));

		CalendarIteratorFactory.recycle(iterator);
		if (negative)
			duration = -duration;
		return (swap == 0) ? duration : - duration; // swap == 0 implies that no swap was done since early date had to be minimum
	}


/**
 * This class manages a pool of calendar iterators.
 *
 */	private static class CalendarIteratorFactory extends BasePoolableObjectFactory {
		private static GenericObjectPool pool =  new GenericObjectPool(new CalendarIteratorFactory());
		public Object makeObject() throws Exception {
			return new CalendarIterator();
		}
		public static CalendarIterator getInstance() {
			try {
				return (CalendarIterator) pool.borrowObject();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}

		public static void recycle(CalendarIterator object) {
			try {
				pool.returnObject(object);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

/**
 * This class is an iterator which is used to return week days or exception days
 *
 */
 	private static class CalendarIterator {
		WorkDay[] exceptions;
		WorkWeek week;
		Calendar scratchDate; // will get reused since this class is recycled

		long exceptionDay;
		int i;
		boolean forward;
		int step;


		private CalendarIterator() {
			scratchDate = DateTime.calendarInstance(); // will get reused since this class is recycled
		}
		/**
		 *
		 */
		private void reverseDirection() {
			if (forward) {
				i -=1;
			} else {
				i += 1;
			}
			step = -step;
			exceptionDay = exceptions[i].getStart();
			forward = !forward;
		}

		private static SimpleDateFormat f = DateTime.dateFormatInstance();


		public long dayOf(long date) {
			scratchDate.setTimeInMillis(date);
			scratchDate.set(Calendar.HOUR_OF_DAY,0);
			scratchDate.set(Calendar.MINUTE,0);
			scratchDate.set(Calendar.SECOND,0); // Fixed rounding bug as we now go to seconds 8/2/07
//			scratchDate.set(Calendar.MILLISECOND,0);
			return scratchDate.getTimeInMillis();
		}

		public long timeOf(long date) {
			return date - dayOf(date);
		}


		private void initialize(CalendarDefinition cal, boolean forward, long day) {
			exceptions = cal.exceptions;
			week = cal.week;
			this.forward = forward;
			scratchDate.setTimeInMillis(day);
			try {
				DateUtils.truncate(scratchDate,Calendar.DATE);
			} catch (Exception e) {
				ErrorLogger.logOnce("hugedate", "date value is garbage " + scratchDate, e);
			}
			step = (forward) ? 1 : -1;
			i = Arrays.binarySearch(exceptions, scratchDate);
			if (i < 0) {// First day not found
				i = -i-1; // set index for the future
				if (!forward)
					i -= 1;
			}
			exceptionDay = exceptions[i].getStart();

		}

		public String dump() {
			String result = "CalendarIterator ";
			result += "weekdays\n";
			for (int i = 0; i < 7; i++) {
				result += "day[" + i +"]" + week.getWeekDay(i) +  "\n";
			}
			result += "There are " + exceptions.length + " exceptions\n";
			for (int j = 0; j < exceptions.length; j++) {
				result += "exception" + exceptions[j].toString();
			}
			return result;

		}

		private WorkingHours getNext(long day) {
			WorkDay workDay;

			if (day == exceptionDay) {
				workDay = exceptions[i]; // move index, save off new value for exception day
				i += step;
				if (i < 0 || i == exceptions.length) {//TODO
					System.out.println("invalid calendar iterator - index is negative or past bounds. avoiding");
					ErrorLogger.logOnce("CalendarIterator","invalid calendar iterator i=" + i,null);
				} else
					exceptionDay = exceptions[i].getStart(); // move index, save off new value for exception day
			} else {
				workDay = week.getWeekDay(dayOfWeek(day));
			}
			return workDay.getWorkingHours();
		}

		private long exceptionDurationDifference(long endDay) {
			long difference = 0;
			if (exceptions.length == 2) // skip sentinels
				return 0;
			while ((forward && exceptionDay < endDay) || (!forward && exceptionDay > endDay)) {
				difference -= week.getWeekDay(dayOfWeek(exceptionDay)).getDuration();
				difference += exceptions[i].getDuration();
				i += step;
				if (i < 0)
					System.out.println("error");
				exceptionDay = exceptions[i].getStart();
			}
			return difference;

		}

		private int dayOfWeek(long day) {
			scratchDate.setTimeInMillis(day);
			return scratchDate.get(Calendar.DAY_OF_WEEK) -1 ;

		}
		private long moveNumberOfDays(int numberOfDays, long fromDay) {
			scratchDate.setTimeInMillis(fromDay);
			scratchDate.add(Calendar.DATE,numberOfDays);
			return scratchDate.getTimeInMillis();
		}

		private long nextDay(long day) {
			scratchDate.setTimeInMillis(day);
			scratchDate.add(Calendar.DATE,forward ? 1 : -1);
			return scratchDate.getTimeInMillis();
		}
		private long prevDay(long day) {
			scratchDate.setTimeInMillis(day);
			scratchDate.add(Calendar.DATE,forward ? -1 : 1);
			return scratchDate.getTimeInMillis();
		}

	}

	/* (non-Javadoc)
	 * @see com.projity.configuration.NamedItem#getName()
	 */
	public String getName() {
		// TODO Auto-generated method stub
		return null;
	}

	/* (non-Javadoc)
	 * @see com.projity.configuration.NamedItem#getCategory()
	 */
	public String getCategory() {
		// TODO Auto-generated method stub
		return null;
	}

	/* (non-Javadoc)
	 * @see com.projity.pm.time.WorkCalendar#setName(java.lang.String)
	 */
	public void setName(String name) {
		// TODO Auto-generated method stub

	}

	/* (non-Javadoc)
	 * @see com.projity.pm.calendar.WorkCalendar#getConcreteInstance()
	 */
	public CalendarDefinition getConcreteInstance() {
		return this; // doesn't make sense to call this
	}
	public static final int getDayOfWeek(long date) {
		Calendar scratchDate = DateTime.calendarInstance();
		scratchDate.setTimeInMillis(date);
		return scratchDate.get(Calendar.DAY_OF_WEEK) -1 ;
	}


	public final WorkDay getWorkDay(long date) {
		WorkDay workDay = null;
		int i = Arrays.binarySearch(getConcreteInstance().exceptions, new Date(date));
		if (i >= 0) {
			workDay =  exceptions[i];
		} else {
			workDay = week.getWeekDay(getDayOfWeek(date));
		}
		return workDay;
	}

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public long getUniqueId() {
        return id;
    }
    public void setUniqueId(long id) {
        this.id = id;
    }
    transient boolean newId=true;
    public boolean isNew(){
    	return newId;
    }
    public void setNew(boolean newId){
    	this.newId=newId;
    }
	/* (non-Javadoc)
	 * @see com.projity.pm.calendar.WorkCalendar#getBaseCalendar()
	 */
	public WorkCalendar getBaseCalendar() {
		return null;
	}

	/* (non-Javadoc)
	 * @see com.projity.pm.calendar.WorkCalendar#dependsOn(com.projity.pm.calendar.WorkCalendar)
	 */
	public boolean dependsOn(WorkCalendar cal) {
		return false;
	}

	/* (non-Javadoc)
	 * @see com.projity.pm.calendar.WorkCalendar#invalidate()
	 */
	public void invalidate() {
	}

	/* (non-Javadoc)
	 * @see com.projity.pm.calendar.WorkCalendar#isInvalid()
	 */
	public boolean isInvalid() {
		return false;
	}


	CalendarDefinition intersectWith(CalendarDefinition other) throws InvalidCalendarIntersectionException {
		CalendarDefinition result = new CalendarDefinition();
		result.week = week.intersectWith(other.week);

		WorkDay exceptionDay;
		// merge exceptions
		for (int i = 0; i < exceptions.length; i++) {
			exceptionDay = exceptions[i];
			result.dayExceptions.add(exceptionDay.intersectWith(other.getWorkDay(exceptionDay.getStart())));
		}
		for (int i = 0; i < other.exceptions.length; i++) {
			exceptionDay = other.exceptions[i];
			result.dayExceptions.add(exceptionDay.intersectWith(getWorkDay(exceptionDay.getStart())));
		}
		result.addSentinelsAndMakeArray();
		return result;
	}

	private transient boolean dirty;
	public boolean isDirty() {
		return dirty;
	}
	public void setDirty(boolean dirty) {
		//System.out.println("CalendarDefinition _setDirty("+dirty+"): "+getName());
		this.dirty = dirty;
	}

	public String dump() {
		String result = "Calendar " + getName() + "\n";
		result += "weekdays\n";
		for (int i = 0; i < 7; i++) {
			result += "day[" + i +"]" + getWeekDay(i) +  "\n";
		}
		result += "There are " + exceptions.length + " exceptions\n";
		for (int j = 0; j < exceptions.length; j++) {
			result += "exception" + exceptions[j].toString();
		}
		return result;

	}

}
