/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/
package org.unitime.timetable.gwt.client.events;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.unitime.timetable.gwt.client.ToolBox;
import org.unitime.timetable.gwt.client.aria.AriaStatus;
import org.unitime.timetable.gwt.client.aria.AriaTextBox;
import org.unitime.timetable.gwt.client.widgets.ServerDateTimeFormat;
import org.unitime.timetable.gwt.client.widgets.UniTimeWidget;
import org.unitime.timetable.gwt.command.client.GwtRpcResponseList;
import org.unitime.timetable.gwt.command.client.GwtRpcService;
import org.unitime.timetable.gwt.command.client.GwtRpcServiceAsync;
import org.unitime.timetable.gwt.resources.GwtAriaMessages;
import org.unitime.timetable.gwt.resources.GwtConstants;
import org.unitime.timetable.gwt.resources.GwtMessages;
import org.unitime.timetable.gwt.shared.AcademicSessionProvider;
import org.unitime.timetable.gwt.shared.AcademicSessionProvider.AcademicSessionChangeEvent;
import org.unitime.timetable.gwt.shared.AcademicSessionProvider.AcademicSessionChangeHandler;
import org.unitime.timetable.gwt.shared.EventInterface.RequestSessionDetails;
import org.unitime.timetable.gwt.shared.EventInterface.SessionMonth;
import org.unitime.timetable.gwt.shared.OnlineSectioningInterface.StudentSectioningContext;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasMouseDownHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.datepicker.client.CalendarUtil;

/**
 * @author Tomas Muller
 */
public class SingleDateSelector extends UniTimeWidget<AriaTextBox> implements HasValue<Date>, HasText, HasEnabled {
	private static final GwtAriaMessages ARIA = GWT.create(GwtAriaMessages.class);
	private static final GwtRpcServiceAsync RPC = GWT.create(GwtRpcService.class);
	private static final GwtConstants CONSTANTS = GWT.create(GwtConstants.class);
	private static final GwtMessages MESSAGES = GWT.create(GwtMessages.class);
	private RegExp[] iRegExp = new RegExp[] {
			RegExp.compile("^([0-9]+)[/ ]*([0-9]*)[/ ]*([0-9]*)$"),
			RegExp.compile("^([0-9]+)\\.?([0-9]*)\\.?([0-9]*)$")
	};

	private PopupPanel iPopup;
	private SingleMonth iMonth;
	private DateTimeFormat iFormat = DateTimeFormat.getFormat(CONSTANTS.eventDateFormat());
	private DateTimeFormat iDefaultFormat = DateTimeFormat.getFormat("MM/dd/yyyy");
	private AcademicSessionProvider iAcademicSession;
	
	private static int sFirstDayOfWeekend;
	private static int sLastDayOfWeekend;
	private static int sStartingDay;
	
	static {
		sFirstDayOfWeekend = (6 + LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().weekendStart()) % 7;
		sLastDayOfWeekend = (6 + LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().weekendEnd()) % 7;
		sStartingDay = (6 + LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().firstDayOfTheWeek()) % 7;
	}
	
	
	AriaTextBox iPicker;
	private boolean iHint;
	
	public SingleDateSelector() {
		this(new AriaTextBox(), null, true);
	}
	
	public SingleDateSelector(AcademicSessionProvider session) {
		this(new AriaTextBox(), session, true);
	}
	
	public SingleDateSelector(AcademicSessionProvider session, boolean hint) {
		this(new AriaTextBox(), session, hint);
	}
	
	private SingleDateSelector(AriaTextBox text, AcademicSessionProvider session, boolean hint) {
		super(text);
		iPicker = getWidget();
		iAcademicSession = session;
		iHint = hint;

		if (iHint) setHint(iFormat.getPattern().toUpperCase());
		iPicker.setStyleName("gwt-SuggestBox");
		iPicker.addStyleName("unitime-DateSelectionBox");
		iPicker.setAriaLabel(null);
		
		iMonth = new SingleMonth(new Date()) {
			@Override
			protected void init() {
				super.init();
				if (iPopup != null && iPopup.isShowing() && getValue() != null) {
					AriaStatus.getInstance().setText(ARIA.singleDateCursor(DateTimeFormat.getFormat(CONSTANTS.singleDateSelectionFormat()).format(getValue())));
				}
			}
		};
		AbsolutePanel panel = new AbsolutePanel();
		panel.setStyleName("unitime-DateSelector");
		panel.add(iMonth);
		
		iPopup = new PopupPanel(true, false);
		iPopup.setPreviewingAllNativeEvents(true);
		iPopup.setStyleName("unitime-DateSelectionBoxPopup");
		iPopup.setWidget(panel);
		
		iPicker.addFocusHandler(new FocusHandler() {
			@Override
			public void onFocus(FocusEvent event) {
				iPopup.showRelativeTo(iPicker);
			}
		});
		
		iPicker.addBlurHandler(new BlurHandler() {
			@Override
			public void onBlur(BlurEvent event) {
				if (iPopup.isShowing()) iPopup.hide();
			}
		});
		
		iPicker.addKeyDownHandler(new KeyDownHandler() {
			@Override
			public void onKeyDown(KeyDownEvent event) {
				if (iPopup.isShowing()) {
					switch (event.getNativeEvent().getKeyCode()) {
					case KeyCodes.KEY_UP:
						iMonth.addDays(-7);
						event.preventDefault();
						event.stopPropagation();
						break;
					case KeyCodes.KEY_DOWN:
						iMonth.addDays(+7);
						event.preventDefault();
						event.stopPropagation();
						break;
					case KeyCodes.KEY_RIGHT:
						if (iPicker.getCursorPos() == iPicker.getText().length()) {
							iMonth.addDays(+1);
							event.preventDefault();
							event.stopPropagation();
						}
						break;
					case KeyCodes.KEY_LEFT:
						if (iPicker.getCursorPos() == 0) {
							iMonth.addDays(-1);
							event.preventDefault();
							event.stopPropagation();
						}
						break;
					case KeyCodes.KEY_PAGEUP:
						iMonth.addMonths(-1);
						event.preventDefault();
						event.stopPropagation();
						break;
					case KeyCodes.KEY_PAGEDOWN:
						iMonth.addMonths(+1);
						event.preventDefault();
						event.stopPropagation();
						break;
					case KeyCodes.KEY_ESCAPE:
						event.preventDefault();
						event.stopPropagation();
						iPopup.hide();
						break;
					case KeyCodes.KEY_ENTER:
						if (iMonth.getValue() != null) {
							iPicker.setText(iFormat.format(iMonth.getValue()));
							ValueChangeEvent.fire(SingleDateSelector.this, getValue());
							AriaStatus.getInstance().setText(ARIA.singleDateSelected(DateTimeFormat.getFormat(CONSTANTS.singleDateSelectionFormat()).format(getValue())));
						}
						event.preventDefault();
						event.stopPropagation();
						iPopup.hide();
						break;
					}
				} else {
					if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_DOWN && (event.getNativeEvent().getAltKey() || iPicker.getCursorPos() == iPicker.getText().length())) {
						try {
							iMonth.setValue(iFormat.parse(iPicker.getText()));
						} catch (Exception e) {}
						iPopup.showRelativeTo(iPicker);
						if (iMonth.getValue() != null) {
							AriaStatus.getInstance().setText(ARIA.singleDatePopupOpenedDateSelected(ARIA.singleDateCursor(DateTimeFormat.getFormat(CONSTANTS.singleDateSelectionFormat()).format(iMonth.getValue()))));
						} else {
							AriaStatus.getInstance().setText(ARIA.singleDatePopupOpenedNoDateSelected(iMonth.getCalendarTitle()));
						}
						event.preventDefault();
						event.stopPropagation();
					}
				}
			}
		});
		
		iMonth.addValueChangeHandler(new ValueChangeHandler<Date>() {
			@Override
			public void onValueChange(ValueChangeEvent<Date> event) {
				if (event.getValue() != null) {
					iPicker.setText(iFormat.format(iMonth.getValue()));
					if (iPopup.isShowing()) iPopup.hide();
					ValueChangeEvent.fire(SingleDateSelector.this, event.getValue());
				}
			}
		});
		
		iPicker.addChangeHandler(new ChangeHandler() {
			@Override
			public void onChange(ChangeEvent event) {
				MatchResult match = iRegExp[0].exec(iPicker.getText());
				int month = -1, day = -1, year = -1;
				if (match != null) {
					month = Integer.parseInt(match.getGroup(1));
					day = (match.getGroup(2).isEmpty() ? 1 : Integer.parseInt(match.getGroup(2)));
					year = (match.getGroup(3).isEmpty() ? -1 : Integer.parseInt(match.getGroup(3)));
					
				} else {
					match = iRegExp[1].exec(iPicker.getText());
					if (match != null) {
						day = Integer.parseInt(match.getGroup(1));
						month = (match.getGroup(2).isEmpty() ? -1 : Integer.parseInt(match.getGroup(2)));
						year = (match.getGroup(3).isEmpty() ? -1 : Integer.parseInt(match.getGroup(3)));
					}
				}
				if (year <= 99 && month >= 0 && iMonth.getMonths() != null) {
					for (SessionMonth m: iMonth.getMonths()) {
						if (m.getMonth() + 1 == month) {
							if (year < 0 || year == m.getYear() + 1900 || year == m.getYear() + 2000) { year = m.getYear(); }
						}
					}
				}
				if (year < 0) {
					year = Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(new Date()));
				} else if (year <= 99) {
					year += 2000;
				}
				if (year >= 0 && month >= 1 && month <= 12 && day >= 1) {
					iMonth.setDate(year, month, day);
					setValue(iMonth.getValue());
					ValueChangeEvent.fire(SingleDateSelector.this, getValue());	
					return;
				}
				
				Date date = null;
				try {
					date = iFormat.parse(iPicker.getText());
				} catch (Exception e) {}
				iMonth.setValue(date);
				setValue(date == null ? null : iMonth.getValue());
				ValueChangeEvent.fire(SingleDateSelector.this, getValue());				
			}
		});
		
		if (iAcademicSession != null) {
			iAcademicSession.addAcademicSessionChangeHandler(new AcademicSessionChangeHandler() {
				@Override
				public void onAcademicSessionChange(AcademicSessionChangeEvent event) {
					if (event.isChanged()) init(event.getNewAcademicSessionId());
				}
			});
			
			Scheduler.get().scheduleDeferred(new ScheduledCommand() {
				@Override
				public void execute() {
					init(iAcademicSession.getAcademicSessionId());
				}
			});
		}
	}
	
	public void setFirstDate(Date firstDate) {
		iMonth.setFirstDate(firstDate == null ? null : iFormat.parse(iFormat.format(firstDate)));
	}
	
	public void setLastDate(Date lastDate) {
		iMonth.setLastDate(lastDate == null ? null : iFormat.parse(iFormat.format(lastDate)));
	}

	public void init(Long sessionId) {
		if (sessionId == null) {
			if (iHint) setHint(MESSAGES.hintNoSession());
		} else {
			if (iHint) {
				if (iAcademicSession != null)
					setHint(MESSAGES.waitLoadingDataForSession(iAcademicSession.getAcademicSessionName()));
				else
					setHint(MESSAGES.waitLoadingData());
			}
			RPC.execute(new RequestSessionDetails(sessionId), new AsyncCallback<GwtRpcResponseList<SessionMonth>>() {

				@Override
				public void onFailure(Throwable caught) {
					setErrorHint(caught.getMessage());
				}

				@Override
				public void onSuccess(GwtRpcResponseList<SessionMonth> result) {
					if (iHint) setHint(iFormat.getPattern().toUpperCase());
					iMonth.setMonths(result);
				}
			});
		}
	}
	
	public void init(final StudentSectioningContext context) {
		if (context == null || context.getAcademicSessionId() == null) {
			if (iHint) setHint(MESSAGES.hintNoSession());
		} else if (context.hasSessionDates()) {
			if (iHint) setHint(iFormat.getPattern().toUpperCase());
			iMonth.setMonths(context.getSessionDates());
		} else {
			if (iHint) {
				if (iAcademicSession != null)
					setHint(MESSAGES.waitLoadingDataForSession(iAcademicSession.getAcademicSessionName()));
				else
					setHint(MESSAGES.waitLoadingData());
			}
			final Long sessionId = context.getAcademicSessionId();
			RPC.execute(new RequestSessionDetails(sessionId), new AsyncCallback<GwtRpcResponseList<SessionMonth>>() {
				@Override
				public void onFailure(Throwable caught) {
					setErrorHint(caught.getMessage());
				}
				@Override
				public void onSuccess(GwtRpcResponseList<SessionMonth> result) {
					if (sessionId.equals(context.getAcademicSessionId()))
						context.setSessionDates(result);
					if (iHint) setHint(iFormat.getPattern().toUpperCase());
					iMonth.setMonths(result);
				}
			});
		}
	}
	
	public void init(GwtRpcResponseList<SessionMonth> result) {
		if (iHint) setHint(iFormat.getPattern().toUpperCase());
		iMonth.setMonths(result);
	}
	
	public static class P extends AbsolutePanel implements HasMouseDownHandlers {
		private String iCaption;
		
		private P(String caption, String... styles) {
			iCaption = caption;
			if (caption != null)
				getElement().setInnerHTML(caption);
			for (String style: styles)
				if (style != null && !style.isEmpty())
					addStyleName(style);
			sinkEvents(Event.ONMOUSEDOWN);
		}
		
		@Override
		public void onBrowserEvent(Event event) {
			switch (DOM.eventGetType(event)) {
		    case Event.ONMOUSEDOWN:
		    	MouseDownEvent.fireNativeEvent(event, this);
		    	event.stopPropagation();
		    	event.preventDefault();
		    	break;
			}
		}
		
		@Override
		public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
			return addHandler(handler, MouseDownEvent.getType());
		}

		public String getCaption() { return iCaption; }
	}
	
	public static class D extends AbsolutePanel implements HasMouseDownHandlers {
		private int iNumber;
		
		private D(int number, String... styles) {
			iNumber = number;
			getElement().setInnerHTML(String.valueOf(number));
			for (String style: styles)
				if (style != null && !style.isEmpty())
					addStyleName(style);
			sinkEvents(Event.ONMOUSEDOWN);
		}
		
		@Override
		public void onBrowserEvent(Event event) {
			switch (DOM.eventGetType(event)) {
		    case Event.ONMOUSEDOWN:
		    	MouseDownEvent.fireNativeEvent(event, this);
		    	event.stopPropagation();
		    	event.preventDefault();
		    	break;
			}
		}
		
		public int getNumber() { return iNumber; }
		
		public String toString() {
			return String.valueOf(1 + getNumber());
		}

		@Override
		public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
			return addHandler(handler, MouseDownEvent.getType());
		}
	}
	
	static int startingDayOfWeek() {
		return sStartingDay;
	}
	
	static boolean isWeekend(int dayOfWeek) {
		return sFirstDayOfWeekend == dayOfWeek || sLastDayOfWeekend == dayOfWeek;
	}
	
	static Date toDate(int year, int month, int day) {
		return DateTimeFormat.getFormat("yyyy/MM/dd").parse(year + "/" + month + "/" + day);
	}
	
	@SuppressWarnings("deprecation")
	static int firstDayOfWeek(int year, int month) {
		return (6 + new Date(year - 1900, month - 1, 1).getDay()) % 7;
	}
	
	@SuppressWarnings("deprecation")
	static int daysInMonth(int year, int month) {
		return new Date(year + (month == 12 ? 1 : 0) - 1900, (month == 12 ? 1 : month + 1) - 1, 0).getDate();
	}
	
	@SuppressWarnings("deprecation")
	static int weekNumber(int year, int month) {
		Date d = new Date(year - 1900, month - 1, 1);
		while (d.getDay() != CalendarUtil.getStartingDayOfWeek()) d.setDate(d.getDate() - 1);
		// ISO 8601: move to the next Thursday
		while (d.getDay() != 4) d.setDate(d.getDate() + 1);
		int y = d.getYear();
		int week = 0;
		while (d.getYear() == y) { d.setDate(d.getDate() - 7); week += 1; }
		return week;
	}
	
	@SuppressWarnings("deprecation")
	static int dayOfYear(int year, int month, int day) {
		Date d = new Date(year - 1900, month - 1, day);
		int doy = 0, y = d.getYear();
		while (d.getYear() == y) { d.setDate(d.getDate() - 1); doy ++; }
		return doy;
	}
	
	@SuppressWarnings("deprecation")
	static Date dayOfYear(int year, int dayOfYear) {
		Date d = new Date(year - 1900, 0, 1); dayOfYear--;
		while (dayOfYear < 0) { d.setDate(d.getDate() - 1); dayOfYear ++; }
		while (dayOfYear > 0) { d.setDate(d.getDate() + 1); dayOfYear --; }
		return d;
	}

	
	static String monthName(int year, int month) {
		return DateTimeFormat.getFormat("MMMM yyyy").format(toDate(year, month, 1));
	}
	
	public static class SingleMonth extends AbsolutePanel implements HasValue<Date> {
		List<D> iDays = new ArrayList<D>();
		int iYear, iMonth, iDay;
		String iTitle = null;
		List<SessionMonth> iMonths = null;
		private boolean iAllowDeselect;
		private Date iFirstDate = null, iLastDate = null;
		
		public SingleMonth() {
			this(null, Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(new Date())),
					 Integer.parseInt(DateTimeFormat.getFormat("MM").format(new Date())),
					 0);
		}
		
		public SingleMonth(String title) {
			this(title, Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(new Date())),
				 Integer.parseInt(DateTimeFormat.getFormat("MM").format(new Date())),
				 0);
		}
		
		public SingleMonth(Date date) {
			this(null, Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(date)),
				 Integer.parseInt(DateTimeFormat.getFormat("MM").format(date)),
				 Integer.parseInt(DateTimeFormat.getFormat("dd").format(date)));
		}
		
		public SingleMonth(String title, int year, int month, int day) {
			iTitle = title;
			iYear = year;
			iMonth = month;
			iDay = day;
			init();
		}
		
		public boolean isAllowDeselect() { return iAllowDeselect; }
		public void setAllowDeselect(boolean allowDeselect) { iAllowDeselect = allowDeselect; }
		
		public void setMonths(List<SessionMonth> months) {
			iMonths = months;
			init();
		}
		
		public List<SessionMonth> getMonths() {
			return iMonths;
		}
		
		public void setFirstDate(Date firstDate) {
			iFirstDate = firstDate;
			init();
		}
		
		public void setLastDate(Date lastDate) {
			iLastDate = lastDate;
			init();
		}
		
		protected void init() {
			clear(); iDays.clear();
			SessionMonth sessionMonth = null;
			boolean hasPrev = false, hasNext = false;
			if (iMonths != null && !iMonths.isEmpty()) {
				for (int i = 0; i < iMonths.size(); i++) {
					SessionMonth m = iMonths.get(i);
					if (m.getMonth() + 1 == iMonth && m.getYear() == iYear) {
						sessionMonth = m;
						if (i > 0) hasPrev = true;
						if (i + 1 < iMonths.size()) hasNext = true;
					}
				}
				if (sessionMonth == null) {
					if (iYear < iMonths.get(0).getYear() || (iYear == iMonths.get(0).getYear() && iMonth <= iMonths.get(0).getMonth())) {
						sessionMonth = iMonths.get(0);
						iYear = sessionMonth.getYear();
						iMonth = sessionMonth.getMonth() + 1;
						hasPrev = false;
						hasNext = iMonths.size() > 1;
						if (iDay > 0) iDay = 1;
					} else {
						sessionMonth = iMonths.get(iMonths.size() - 1);
						iYear = sessionMonth.getYear();
						iMonth = sessionMonth.getMonth() + 1;
						hasPrev = iMonths.size() > 1;
						hasNext = false;
						if (iDay > 0) iDay = daysInMonth(iYear, iMonth);
					}
				}
			}
			if (sessionMonth != null && iDay >= 0) {
				while (iDay > 0 && sessionMonth.hasFlag(iDay - 1, SessionMonth.Flag.DISABLED)) iDay --;
				if (iDay == 0) {
					iDay = 1;
					while (iDay <= daysInMonth(iYear, iMonth) && sessionMonth.hasFlag(iDay - 1, SessionMonth.Flag.DISABLED)) iDay ++;
					if (iDay > daysInMonth(iYear, iMonth)) iDay = 0; // all disabled
				}
			}
			
			int firstDayOfWeek = firstDayOfWeek(iYear, iMonth);
			int nrDays = daysInMonth(iYear, iMonth);
			int firstWeekNumber = weekNumber(iYear, iMonth);
			
			addStyleName("month");
						
			if (iTitle != null) {
				P box = new P(null, "box");
				add(box);
				P row = new P(null, "row");
				box.add(row);
				row.add(new P(iTitle, "command"));
			}
			
			P box = new P(null, "box");
			add(box);

			P top = new P(null, "row");
			if (sessionMonth == null) {
				P py = new P("&laquo;", "cell", "left", "clickable");
				py.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						addMonths(-12);
					}
				});
				top.add(py);
				P pm = new P("&lsaquo;", "cell", "left", "clickable");
				top.add(pm);
				pm.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						addMonths(-1);
					}
				});
				P m = new P(monthName(iYear, iMonth), "cell", "label", "middle", "clickable");
				m.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						setDate(new Date());
					}
				});
				top.add(m);
				P nm = new P("&rsaquo;", "cell", "right", "clickable");
				nm.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						addMonths(+1);
					}
				});
				top.add(nm);
				P ny = new P("&raquo;", "cell", "right", "clickable");
				ny.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						addMonths(+12);
					}
				});
				top.add(ny);				
			} else {
				top.add(new P(null, "cell", "left"));
				if (hasPrev) {
					P pm = new P("&lsaquo;", "cell", "left", "clickable");
					top.add(pm);
					pm.addMouseDownHandler(new MouseDownHandler() {
						@Override
						public void onMouseDown(MouseDownEvent event) {
							addMonths(-1);
						}
					});
				} else {
					top.add(new P(null, "cell", "left"));
				}
				P m = new P(monthName(iYear, iMonth), "cell", "label", "middle", "clickable");
				m.addMouseDownHandler(new MouseDownHandler() {
					@Override
					public void onMouseDown(MouseDownEvent event) {
						setDate(new Date());
					}
				});
				top.add(m);
				if (hasNext) {
					P nm = new P("&rsaquo;", "cell", "right", "clickable");
					nm.addMouseDownHandler(new MouseDownHandler() {
						@Override
						public void onMouseDown(MouseDownEvent event) {
							addMonths(+1);
						}
					});
					top.add(nm);
				} else {
					top.add(new P(null, "cell", "right"));
				}
				top.add(new P(null, "cell", "right"));
			}

			box.add(top);
			
			box = new P(null, "box");
			add(box);
			
			P header = new P(null, "row");
			box.add(header);
			P corner = new P(null, "cell", "corner");
			header.add(corner);
			
			for (int i = 0; i < 7; i++) {
				header.add(new P(CONSTANTS.days()[(i + startingDayOfWeek()) % 7], "cell", "dow"));
			}

			int weekNumber = firstWeekNumber;
			P line = new P(null, "row");
			box.add(line);
			P week = new P(String.valueOf(weekNumber ++), "cell", "week");
			line.add(week);
			
			int idx = 0;
			int blanks = (firstDayOfWeek + 7 - startingDayOfWeek()) % 7;
			for (int i = 0; i < blanks; i++) {
				line.add(new P(null, "cell", (i + 1 == blanks ? "last-blank": "blank")));
				idx++;
			}
			
			MouseDownHandler onClick = new MouseDownHandler() {
				@Override
				public void onMouseDown(MouseDownEvent event) {
					if (iDay == ((D)event.getSource()).getNumber() && iAllowDeselect) {
						D old = (iDay <= 0 || iDay > iDays.size() ? null : iDays.get(iDay - 1));
						if (old != null) old.removeStyleName("selected");
						iDay = 0;
						ValueChangeEvent.fire(SingleMonth.this, getValue());
					} else {
						D old = (iDay <= 0 || iDay > iDays.size() ? null : iDays.get(iDay - 1));
						if (old != null) old.removeStyleName("selected");
						iDay = ((D)event.getSource()).getNumber();
						iDays.get(iDay - 1).addStyleName("selected");
						ValueChangeEvent.fire(SingleMonth.this, getValue());
					}
				}
			};
			
			int today = -1;
			if (iYear == Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(new Date())) &&
				iMonth == Integer.parseInt(DateTimeFormat.getFormat("MM").format(new Date())))
				today = Integer.parseInt(DateTimeFormat.getFormat("dd").format(new Date()));
			for (int i = 1; i <= nrDays; i++) {
				if (i > 1 && idx % 7 == 0) {
					if (idx == 7 && iMonth == 1 && weekNumber > 50) weekNumber = 1;
					line = new P(null, "row");
					box.add(line);
					week = new P(String.valueOf(weekNumber ++), "cell", "week");
					line.add(week);
				}
				D d = new D(i, "cell", (isWeekend((idx + startingDayOfWeek()) % 7) ? "weekend" : "day"), "clickable", (iDay == i ? "selected" : null));
				line.add(d);
				iDays.add(d);
				idx++;
				boolean enabled = true;
				if (i == today)
					d.addStyleName("today");
				if (sessionMonth != null) {
					if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.START))
						d.addStyleName("start");
					else if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.END))
						d.addStyleName("start");
					else if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.FINALS))
						d.addStyleName("exam");
					else if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.MIDTERMS))
						d.addStyleName("midterm");
					else if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.HOLIDAY))
						d.addStyleName("holiday");
					else if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.BREAK))
						d.addStyleName("break");
					if (sessionMonth.hasFlag(i - 1, SessionMonth.Flag.DISABLED)) {
						d.removeStyleName("clickable");
						d.addStyleName("disabled");
						enabled = false;
					}
				}
				if (enabled && iFirstDate != null && DateTimeFormat.getFormat("yyyy/MM/dd").parse(iYear + "/" + iMonth + "/" + i).before(iFirstDate)) {
					d.removeStyleName("clickable");
					d.addStyleName("disabled");
					enabled = false;
				}
				if (enabled && iLastDate != null && DateTimeFormat.getFormat("yyyy/MM/dd").parse(iYear + "/" + iMonth + "/" + i).after(iLastDate)) {
					d.removeStyleName("clickable");
					d.addStyleName("disabled");
					enabled = false;
				}
				if (enabled)
					d.addMouseDownHandler(onClick);
			}
		}
		
		public void addMonths(int months) {
			iMonth += months;
			if (iMonth <= 0) { iYear -= 1; iMonth += 12; }
			if (iMonth > 12) { iYear += 1; iMonth -= 12; }
			init();
		}
		
		@SuppressWarnings("deprecation")
		public void addDays(int days) {
			Date d = getValue();
			if (d == null) {
				setDate(new Date());
			} else {
				d.setDate(d.getDate() + days);
				setDate(d);
			}
		}
		
		public void clearSelection() {
			D old = (iDay <= 0 || iDay > iDays.size() ? null : iDays.get(iDay - 1));
			if (old != null) {
				old.removeStyleName("selected");
				iDay = 0;
			}
		}
		
		public void setDate(Date date) {
			iYear =  Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(date));
			iMonth = Integer.parseInt(DateTimeFormat.getFormat("MM").format(date));
			iDay = Integer.parseInt(DateTimeFormat.getFormat("dd").format(date));
			init();
		}
		
		public void setDate(int year, int month, int day) {
			iYear = year; iMonth = month; iDay = day;
			init();
		}
		
		@Override
		public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Date> handler) {
			return addHandler(handler, ValueChangeEvent.getType());
		}

		@Override
		public Date getValue() {
			if (iDay <= 0 || iDay > iDays.size()) return null;
			return DateTimeFormat.getFormat("yyyy/MM/dd").parse(iYear + "/" + iMonth + "/" + iDay);
		}
		
		@Override
		public void setValue(Date value) {
			setValue(value, false);
		}

		@Override
		public void setValue(Date value, boolean fireEvents) {
			iYear =  Integer.parseInt(DateTimeFormat.getFormat("yyyy").format(value == null ? new Date() : value));
			iMonth = Integer.parseInt(DateTimeFormat.getFormat("MM").format(value == null ? new Date() : value));
			iDay = (value == null ? 0 : Integer.parseInt(DateTimeFormat.getFormat("dd").format(value)));
			init();
			if (fireEvents)
				ValueChangeEvent.fire(this, value);				
		}
		
		public String toString() {
			return (getValue() == null ? "" : DateTimeFormat.getFormat(CONSTANTS.eventDateFormat()).format(getValue()));
		}
		
		public String getCalendarTitle() { return iTitle; }
	}

	@Override
	public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Date> handler) {
		return addHandler(handler, ValueChangeEvent.getType());
	}

	@Override
	public Date getValue() {
		try {
			return iFormat.parse(iPicker.getText());
		} catch (Exception e) {
			return null;
		}
	}

	@Override
	public void setValue(Date value) {
		setValue(value, false);
	}

	@Override
	public void setValue(Date value, boolean fireEvents) {
		if (value == null) {
			iPicker.setText("");
			iMonth.setValue(null);
		} else {
			iPicker.setText(iFormat.format(value));
			iMonth.setValue(value);
		}
		if (fireEvents)
			ValueChangeEvent.fire(this, value);
	}
	
	public void setValueInServerTimeZone(Date value) {
		setValue(ServerDateTimeFormat.toLocalDate(value));
	}
	
	public Date getValueInServerTimeZone() {
		return ServerDateTimeFormat.toServerDate(getValue());
	}
	
	@Override
	public void setText(String text) {
		if (text == null || text.isEmpty())
			setValue(null);
		else {
			try {
				setValue(iFormat.parse(text));
			} catch (IllegalArgumentException e) {
				setValue(iDefaultFormat.parse(text));
			}
		}
	}

	@Override
	public String getText() {
		return iPicker.getText();
	}
	
	public Date today() {
		return iFormat.parse(iFormat.format(new Date()));
	}
	
	public static SingleDateSelector insert(RootPanel panel) {
		String format = panel.getElement().getAttribute("format");
		final String onchange = panel.getElement().getAttribute("onchange");
		String error = panel.getElement().getAttribute("error");
		String disabled = panel.getElement().getAttribute("disabled");
		AriaTextBox text = new AriaTextBox(panel.getElement().getFirstChildElement());
		SingleDateSelector selector = new SingleDateSelector(text, null, false);
		if (format != null && !format.isEmpty())
			selector.iFormat = DateTimeFormat.getFormat(format);
		else
			selector.iFormat = DateTimeFormat.getFormat(CONSTANTS.dateEntryFormat());
		if (onchange != null) {
			panel.getElement().removeAttribute("onchange");
			selector.addValueChangeHandler(new ValueChangeHandler<Date>() {
				@Override
				public void onValueChange(ValueChangeEvent<Date> event) {
					ToolBox.eval(onchange);
				}
			});
		}
		if (text.getText() != null && !text.getText().isEmpty()) {
			Date date = null;
			try {
				date = selector.iFormat.parse(text.getText());
			} catch (IllegalArgumentException e) {}
			if (date != null)
				selector.setValue(date);
		}
		if (error != null && !error.isEmpty())
			selector.setErrorHint(error);
		if (disabled != null && "true".equalsIgnoreCase(disabled))
			selector.setEnabled(false);
		panel.add(selector);
		return selector;
	}
	
	public void setFormat(DateTimeFormat format) {
		iFormat = format; 
		if (iHint) setHint(iFormat.getPattern().toUpperCase());
	}
	
	public String getFormat() {
		return iFormat.getPattern();
	}

	@Override
	public boolean isEnabled() {
		return iPicker.isEnabled();
	}

	@Override
	public void setEnabled(boolean enabled) {
		iPicker.setEnabled(enabled);
	}
	
	public boolean isPopupShowing() {
		return iPopup.isShowing();
	}
}
