/*
 * 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.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.unitime.timetable.gwt.client.Client;
import org.unitime.timetable.gwt.client.Lookup;
import org.unitime.timetable.gwt.client.ToolBox;
import org.unitime.timetable.gwt.client.aria.AriaTabBar;
import org.unitime.timetable.gwt.client.events.AcademicSessionSelectionBox.AcademicSession;
import org.unitime.timetable.gwt.client.events.AcademicSessionSelectionBox.AcademicSessionFilter;
import org.unitime.timetable.gwt.client.events.EventMeetingTable.EventMeetingRow;
import org.unitime.timetable.gwt.client.events.StartEndTimeSelector.StartEndTime;
import org.unitime.timetable.gwt.client.events.TimeGrid.MeetingClickEvent;
import org.unitime.timetable.gwt.client.events.TimeGrid.MeetingClickHandler;
import org.unitime.timetable.gwt.client.page.UniTimeNotifications;
import org.unitime.timetable.gwt.client.page.UniTimePageHeader;
import org.unitime.timetable.gwt.client.page.UniTimePageLabel;
import org.unitime.timetable.gwt.client.widgets.FilterBox;
import org.unitime.timetable.gwt.client.widgets.IntervalSelector;
import org.unitime.timetable.gwt.client.widgets.LoadingWidget;
import org.unitime.timetable.gwt.client.widgets.SimpleForm;
import org.unitime.timetable.gwt.client.widgets.ServerDateTimeFormat;
import org.unitime.timetable.gwt.client.widgets.TimeSelector.TimeUtils;
import org.unitime.timetable.gwt.client.widgets.UniTimeDialogBox;
import org.unitime.timetable.gwt.client.widgets.UniTimeHeaderPanel;
import org.unitime.timetable.gwt.client.widgets.UniTimeWidget;
import org.unitime.timetable.gwt.client.widgets.FilterBox.Chip;
import org.unitime.timetable.gwt.client.widgets.UniTimeTable.MouseClickListener;
import org.unitime.timetable.gwt.client.widgets.UniTimeTable.TableEvent;
import org.unitime.timetable.gwt.client.widgets.WeekSelector;
import org.unitime.timetable.gwt.command.client.GwtRpcResponse;
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.EventInterface;
import org.unitime.timetable.gwt.shared.EventInterface.ContactInterface;
import org.unitime.timetable.gwt.shared.EventInterface.FilterRpcResponse;
import org.unitime.timetable.gwt.shared.EventInterface.RequestSessionDetails;
import org.unitime.timetable.gwt.shared.EventInterface.SaveFilterDefaultRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.SaveOrApproveEventRpcResponse;
import org.unitime.timetable.gwt.shared.EventInterface.SelectionInterface;
import org.unitime.timetable.gwt.shared.PersonInterface;
import org.unitime.timetable.gwt.shared.AcademicSessionProvider.AcademicSessionChangeEvent;
import org.unitime.timetable.gwt.shared.AcademicSessionProvider.AcademicSessionChangeHandler;
import org.unitime.timetable.gwt.shared.EventInterface.ApprovalStatus;
import org.unitime.timetable.gwt.shared.EventInterface.ApproveEventRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.EncodeQueryRpcResponse;
import org.unitime.timetable.gwt.shared.EventInterface.EventDetailRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.EventType;
import org.unitime.timetable.gwt.shared.EventInterface.FilterRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.MeetingInterface;
import org.unitime.timetable.gwt.shared.EventInterface.EncodeQueryRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.EventPropertiesRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.EventLookupRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.MessageInterface;
import org.unitime.timetable.gwt.shared.EventInterface.ResourceLookupRpcRequest;
import org.unitime.timetable.gwt.shared.EventInterface.ResourceInterface;
import org.unitime.timetable.gwt.shared.EventInterface.ResourceType;
import org.unitime.timetable.gwt.shared.EventInterface.EventPropertiesRpcResponse;
import org.unitime.timetable.gwt.shared.EventInterface.SessionMonth;
import org.unitime.timetable.gwt.shared.EventInterface.WeekInterface;

import com.google.gwt.aria.client.Id;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.TakesValue;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.Location;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;

/**
 * @author Tomas Muller
 */
public class EventResourceTimetable extends Composite implements EventMeetingTable.MeetingFilter, EventAdd.EventPropertiesProvider, AcademicSessionFilter {
	private static GwtAriaMessages ARIA = GWT.create(GwtAriaMessages.class);
	private static final GwtConstants CONSTANTS = GWT.create(GwtConstants.class);
	private static final GwtMessages MESSAGES = GWT.create(GwtMessages.class);
	private static final GwtRpcServiceAsync RPC = GWT.create(GwtRpcService.class);
	
	private SimplePanel iRootPanel;
	
	private EventDetail iEventDetail;
	private EventAdd iEventAdd;
	
	private SimpleForm iPanel, iFilter;
	private DockPanel iDockPanel;
	private SimplePanel iGridOrTablePanel;
	private UniTimeHeaderPanel iHeader, iFooter, iFilterHeader;
	private TimeGrid iTimeGrid;
	private EventMeetingTable iTable;
	private ScrollPanel iTablePanel;
	private ResourceInterface iResource;
	private List<EventInterface> iData;
	private WeekSelector iWeekPanel;
	private RoomSelector iRoomPanel;
	private AcademicSessionSelectionBox iSession;
	private ListBox iResourceTypes;
	private SuggestBox iResources;
	private int iResourcesRow = -1;
	private EventFilterBox iEvents = null;
	private RoomFilterBox iRooms = null;
	private String iLocDate = null, iLocRoom = null;
	private MeetingClickHandler iMeetingClickHandler;
	private Lookup iLookup;
	private AriaTabBar iTabBar;
	private ApproveDialog iApproveDialog;
	private int iSessionRow = -1;
	private Set<Integer> iMatchingWeeks = new HashSet<Integer>();
	private Set<Long> iMatchingRooms = new HashSet<Long>();
	
	private EventPropertiesRpcResponse iProperties = null;
	private HistoryToken iHistoryToken = null;
	private PageType iType = null;
	private boolean iInitialized = false;
	private List<EventInterface> iBack = new ArrayList<EventInterface>();
	private List<SessionMonth> iSessionMonths = null;
	
	public static enum PageType {
		Timetable("tab", "0", "title", MESSAGES.pageEventTimetable(), "rooms", "", "showClear", "true"),
		Events("filter", "events", "rooms", "flag:Event", "events", "mode:\"My Events\"", "type", "room", "title", MESSAGES.pageEvents(),
				"fixedTitle", "true", "fixedType", "true", "tab", "1", "showClear", "true", "saveAsDefault", "events,rooms"),
		RoomTimetable("type", "room", "fixedType", "true", "title", MESSAGES.pageRoomTimetable(), "showClear", "true"),
		Classes(
				"type", "subject", "fixedType", "true", "events", "type:Class", "tab", "1", "filter", "classes",
				"rooms", "", "title", MESSAGES.pageClasses(), "fixedTitle", "true", "addEvent", "false", "showFilter", "false", "showClear", "false"),
		Exams(
				"type", "subject", "fixedType", "true", "events", "type:\"Final Exam\" type:\"Midterm Exam\"",
				"tab", "1", "filter", "exams", "rooms", "", "title", MESSAGES.pageExaminations(), "fixedTitle", "true", "addEvent", "false", "showFilter", "false", "showClear", "false"),
		Personal(
				"type", "person", "fixedType", "true", "events", "", "filter", "person", "rooms", "", "title", MESSAGES.pagePersonalTimetable(),
				"addEvent", "false", "fixedTitle", "true", "showFilter", "false", "showClear", "false"
				),
		Availability("title", MESSAGES.pageEventRoomAvailability(), "rooms", "flag:Event");
		
		
		String[] iParams = null; 
		PageType(String... params) { iParams = params; }
		
		public String[] getParams() { return iParams; }
	}
	
	public EventResourceTimetable(PageType type) {
		iHistoryToken = new HistoryToken(type);
		iType = type;
		
		iFilter = new SimpleForm(2);
		iFilter.removeStyleName("unitime-NotPrintableBottomLine");
		iFilter.getColumnFormatter().setWidth(0, "120px");
		
		iFilterHeader = new UniTimeHeaderPanel(MESSAGES.sectFilter());
		iLookup = new Lookup();
		iLookup.setOptions("mustHaveExternalId");
		iLookup.addValueChangeHandler(new ValueChangeHandler<PersonInterface>() {
			@Override
			public void onValueChange(ValueChangeEvent<PersonInterface> event) {
				if (event.getValue() != null) {
					iResources.setText(event.getValue().getId());
					resourceTypeChanged(true);
				}
			}
		});
		iFilterHeader.addButton("lookup", MESSAGES.buttonLookup(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				iLookup.center();
			}
		});
		iFilterHeader.setEnabled("lookup", false);
		iFilterHeader.addButton("add", MESSAGES.buttonAddEvent(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				iEventAdd.setEvent(null);
				iEventAdd.show();
			}
		});
		iFilterHeader.setEnabled("add", false);
		iFilter.addHeaderRow(iFilterHeader);
		iFilterHeader.addButton("clear", MESSAGES.buttonClear(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				// Event filter
				iEvents.setValue(iHistoryToken.getDefaultParameter("events", ""), true);
				
				// Room filter
				iRooms.setValue(iHistoryToken.getDefaultParameter("rooms", ""), true);
				
				// Resource type
				if (!"true".equals(iHistoryToken.getParameter("fixedType", "false"))) {
					String typeString = iHistoryToken.getDefaultParameter("type", "room");
					if (typeString != null)
						for (int idx = 0; idx < iResourceTypes.getItemCount(); idx ++) {
							if (iResourceTypes.getValue(idx).equalsIgnoreCase(typeString)) {
								iResourceTypes.setSelectedIndex(idx);
							}
						}					
				}
				
				// Subject, curriculum, department, etc.
				iResources.setValue(iHistoryToken.getDefaultParameter("name", ""));

				// Room selection
				iLocRoom = iHistoryToken.getDefaultParameter("room", "");
				
				// Week selection
				iLocDate = iHistoryToken.getDefaultParameter("date", "");

				hideResults();
				changeUrl();
				UniTimePageLabel.getInstance().setPageName(getPageName());
				resourceTypeChanged(false);
			}
		});
		iFilterHeader.setEnabled("clear", "true".equals(iHistoryToken.getParameter("showClear", "true")));
		iFilterHeader.addButton("search", MESSAGES.buttonSearch(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				resourceTypeChanged(true);
			}
		});

		iSession = new AcademicSessionSelectionBox(iHistoryToken.getParameter("term"), "Events") {
			@Override
			protected void onInitializationSuccess(List<AcademicSession> sessions) {
				iFilter.setVisible(sessions != null && !sessions.isEmpty());
				UniTimePageHeader.getInstance().getRight().setVisible(false);
				UniTimePageHeader.getInstance().getRight().setPreventDefault(true);
			}
			
			@Override
			protected void onInitializationFailure(Throwable caught) {
				UniTimeNotifications.error(MESSAGES.failedLoadSessions(caught.getMessage()), caught);
			}
		};
		iSession.setFilter(this);
		iSessionRow = iFilter.addRow(MESSAGES.propAcademicSession(), iSession);
		
		iEvents = new EventFilterBox(iSession);
		if ("true".equals(iHistoryToken.getParameter("showFilter", "true")))
			iFilter.addRow(MESSAGES.propEventFilter(), iEvents);
		
		iRooms = new RoomFilterBox(iSession);
		if ("true".equals(iHistoryToken.getParameter("showFilter", "true")))
			iFilter.addRow(MESSAGES.propRoomFilter(), iRooms);
		
		iResourceTypes = new ListBox();
		for (ResourceType resource: ResourceType.values()) {
			if (resource.isVisible())
				iResourceTypes.addItem(resource.getResourceType(CONSTANTS), resource.name());
		}
		
		iResourceTypes.addChangeHandler(new ChangeHandler() {
			@Override
			public void onChange(ChangeEvent event) {
				iResources.setText("");
				UniTimePageLabel.getInstance().setPageName(getPageName());
				hideResults();
				if (iProperties != null)
					resourceTypeChanged(isShowingResults());
			}
		});
		int row = iFilter.addRow(MESSAGES.propResourceType(), iResourceTypes);
		if ("true".equals(iHistoryToken.getParameter("fixedType", "false")))
			iFilter.getRowFormatter().setVisible(row, false);
		
		iResources = new SuggestBox(new SuggestOracle() {
			@Override
			public void requestDefaultSuggestions(Request request, Callback callback) {
				requestSuggestions(request, callback);
			}
			@Override
			public void requestSuggestions(final Request request, final Callback callback) {
				if (iSession.getAcademicSessionId() != null) {
					RPC.execute(
							ResourceLookupRpcRequest.findResources(iSession.getAcademicSessionId(),
									ResourceType.valueOf(iResourceTypes.getValue(iResourceTypes.getSelectedIndex())),
									request.getQuery(),
									request.getLimit()), new AsyncCallback<GwtRpcResponseList<ResourceInterface>>() {
								@Override
								public void onFailure(final Throwable caught) {
									ArrayList<Suggestion> suggestions = new ArrayList<Suggestion>();
									suggestions.add(new Suggestion() {
										@Override
										public String getDisplayString() {
											return "<font color='red'>" + caught.getMessage() + "</font>";
										}
										@Override
										public String getReplacementString() {
											return "";
										}});
									callback.onSuggestionsReady(request, new Response(suggestions));
									ToolBox.checkAccess(caught);
								}
								@Override
								public void onSuccess(GwtRpcResponseList<ResourceInterface> result) {
									ArrayList<Suggestion> suggestions = new ArrayList<Suggestion>();
									for (ResourceInterface resource: result) {
										suggestions.add(new ResourceSuggestion(resource));
									}
									callback.onSuggestionsReady(request, new Response(suggestions));
								}
							});
				}
			}
			@Override
			public boolean isDisplayStringHTML() { return true; }
			});
		iResources.getValueBox().addFocusHandler(new FocusHandler() {
			@Override
			public void onFocus(FocusEvent event) {
				iResources.showSuggestionList();
			}
		});
		iResourcesRow = iFilter.addRow(MESSAGES.propResource(), iResources);
		iResources.addSelectionHandler(new SelectionHandler<Suggestion>() {
			public void onSelection(SelectionEvent<Suggestion> event) {
				if (event.getSelectedItem() instanceof ResourceSuggestion) {
					resourceChanged(((ResourceSuggestion)event.getSelectedItem()).getResource());
				}
			}
		});
		iFilter.getRowFormatter().setVisible(iResourcesRow, false);

		iResourceTypes.setSelectedIndex(ResourceType.ROOM.ordinal());
		
		iPanel = new SimpleForm(2);
		iPanel.removeStyleName("unitime-NotPrintableBottomLine");
		iPanel.addRow(iFilter);
		iHeader = new UniTimeHeaderPanel();
		iPanel.addHeaderRow(iHeader);
		iWeekPanel = new WeekSelector(iSession);
		iWeekPanel.addValueChangeHandler(new ValueChangeHandler<WeekSelector.Interval>() {
			@Override
			public void onValueChange(ValueChangeEvent<WeekSelector.Interval> e) {
				iLocDate = iWeekPanel.getSelection();
				tabOrDataChanged(true);
			}
		});
		iWeekPanel.setFilter(new WeekSelector.Filter<WeekInterface>() {
			@Override
			public boolean filter(WeekInterface week) {
				return !iMatchingWeeks.contains(week.getDayOfYear());
			}
			@Override
			public boolean isEmpty() {
				return iMatchingWeeks.isEmpty();
			}
		});
		iRoomPanel = new RoomSelector();
		iRoomPanel.setFilter(new WeekSelector.Filter<ResourceInterface>() {
			@Override
			public boolean filter(ResourceInterface room) {
				return !iMatchingRooms.contains(room.getId());
			}
			@Override
			public boolean isEmpty() {
				return iMatchingRooms.isEmpty();
			}
		});
		
		iTabBar = new AriaTabBar();
		iTabBar.addTab(MESSAGES.tabGrid(), true);
		iTabBar.addTab(MESSAGES.tabEventTable(), true);
		iTabBar.addTab(MESSAGES.tabMeetingTable(), true);
		iTabBar.addSelectionHandler(new SelectionHandler<Integer>() {
			@Override
			public void onSelection(SelectionEvent<Integer> event) {
				tabOrDataChanged(true);
			}
		});
		
		iGridOrTablePanel = new SimplePanel();
		iGridOrTablePanel.setStyleName("unitime-TabPanel");
		
		final Character gridAccessKey = UniTimeHeaderPanel.guessAccessKey(MESSAGES.tabGrid());
		final Character eventsAccessKey = UniTimeHeaderPanel.guessAccessKey(MESSAGES.tabEventTable());
		final Character meetingsAccessKey = UniTimeHeaderPanel.guessAccessKey(MESSAGES.tabMeetingTable());
		if (gridAccessKey != null || eventsAccessKey != null || meetingsAccessKey != null) {
			RootPanel.get().addDomHandler(new KeyUpHandler() {
				@Override
				public void onKeyUp(KeyUpEvent event) {
					if (!iRootPanel.getWidget().equals(iPanel)) return;
					int tab = -1;
					if (gridAccessKey != null && event.getNativeEvent().getCtrlKey() && (
							event.getNativeKeyCode() == gridAccessKey || event.getNativeKeyCode() == Character.toUpperCase(gridAccessKey))) {
						tab = 0;
					}
					if (eventsAccessKey != null && event.getNativeEvent().getCtrlKey() && (
							event.getNativeKeyCode() == eventsAccessKey || event.getNativeKeyCode() == Character.toUpperCase(eventsAccessKey))) {
						tab = 1;
					}
					if (meetingsAccessKey != null && event.getNativeEvent().getCtrlKey() && (
							event.getNativeKeyCode() == meetingsAccessKey || event.getNativeKeyCode() == Character.toUpperCase(meetingsAccessKey))) {
						tab = 2;
					}
					if (tab >= 0) {
						iTabBar.selectTab(tab);
						event.preventDefault();
					}
				}
			}, KeyUpEvent.getType());
		}
		
		iDockPanel = new DockPanel();
		iDockPanel.setStyleName("unitime-EventResults");
		iDockPanel.setSpacing(0);
		iDockPanel.add(iGridOrTablePanel, DockPanel.SOUTH);
		iDockPanel.add(iRoomPanel, DockPanel.WEST);
		iDockPanel.setCellHorizontalAlignment(iRoomPanel, HasHorizontalAlignment.ALIGN_LEFT);
		iDockPanel.add(iTabBar, DockPanel.CENTER);
		iDockPanel.setCellVerticalAlignment(iTabBar, HasVerticalAlignment.ALIGN_BOTTOM);
		iDockPanel.setCellHorizontalAlignment(iTabBar, HasHorizontalAlignment.ALIGN_CENTER);
		iDockPanel.add(iWeekPanel, DockPanel.EAST);
		iDockPanel.setCellHorizontalAlignment(iWeekPanel, HasHorizontalAlignment.ALIGN_RIGHT);
		Roles.getTabpanelRole().set(iDockPanel.getElement());
		Roles.getTabpanelRole().setAriaOwnsProperty(iDockPanel.getElement(), Id.of(iTabBar.getElement()));

		iPanel.addRow(iDockPanel);
		
		hideResults();
		
		iFooter = iHeader.clonePanel();
		iPanel.addRow(iFooter);
		iRootPanel = new SimplePanel(iPanel);
		initWidget(iRootPanel);
		
		iRoomPanel.addValueChangeHandler(new ValueChangeHandler<IntervalSelector<ResourceInterface>.Interval>() {
			@Override
			public void onValueChange(ValueChangeEvent<IntervalSelector<ResourceInterface>.Interval> e) {
				iLocRoom = iRoomPanel.getSelection();
				tabOrDataChanged(true);
			}
		});

		for (int i = 1; i < iPanel.getRowCount(); i++)
			iPanel.getRowFormatter().setVisible(i, false);
					
		iHeader.addButton("add", MESSAGES.buttonAddEvent(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				iEventAdd.setEvent(null);
				iEventAdd.show();
			}
		});
		iHeader.setEnabled("add", false);
		iHeader.addButton("print", MESSAGES.buttonPrint(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent clickEvent) {
				final EventMeetingTable table = new EventMeetingTable(getSelectedTab() <= 1 ? EventMeetingTable.Mode.ListOfEvents : EventMeetingTable.Mode.ListOfMeetings, false, EventResourceTimetable.this);
				table.setMeetingFilter(EventResourceTimetable.this);
				table.setShowMainContact(iProperties != null && iProperties.isCanLookupMainContact());
				table.setShowMeetingContacts(iProperties != null && iProperties.isCanViewMeetingContacts());
				table.setEvents(iData);
				table.setSortBy(iTable.getSortBy());
				table.getElement().getStyle().setWidth(1040, Unit.PX);
				
				int firstSlot = 12 + CONSTANTS.eventStartDefault(), lastSlot = CONSTANTS.eventStopDefault();
				boolean skipDays = iEvents.hasChip(new FilterBox.Chip("day", null));
				boolean hasDay[] = new boolean[7];
				for (int i = 0; i < 7; i++) {
					int dayInv = (7 + i - getFirstDayOfWeek()) % 7;
					if (dayInv < 5)
						hasDay[i] = !skipDays || iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[i]));
					else
						hasDay[i] = iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[i]));
				}
				for (EventInterface event: iData) {
					if (event.getType() == EventType.Unavailabile && !iEvents.hasChip(new Chip("type", "Not Available"))) continue;
					for (MeetingInterface meeting: event.getMeetings()) {
						if (filterEvent(event, meeting)) continue;
						if (meeting.getApprovalStatus() != ApprovalStatus.Pending && meeting.getApprovalStatus() != ApprovalStatus.Approved) continue;
						if (firstSlot > meeting.getStartSlot()) firstSlot = meeting.getStartSlot();
						if (lastSlot < meeting.getEndSlot()) lastSlot = meeting.getEndSlot();
						hasDay[meeting.getDayOfWeek()] = true;
					}
				}
				Chip after = iEvents.getChip("after");
				if (after != null) {
					Integer slot = TimeUtils.parseTime(CONSTANTS, after.getValue(), null);
					if (slot != null && firstSlot > slot) firstSlot = slot;
				}
				Chip before = iEvents.getChip("before");
				if (before != null) {
					Integer slot = TimeUtils.parseTime(CONSTANTS, before.getValue(), Math.min(firstSlot, CONSTANTS.eventStartDefault()));
					if (slot != null && lastSlot < slot) lastSlot = slot;
				}
				int nrDays = 0;
				for (boolean d: hasDay) if (d) nrDays++;
				int days[] = new int[nrDays];
				int d = 0;
				for (int i = 0; i < 7; i++) {
					int day = (i + getFirstDayOfWeek()) % 7;
					if (hasDay[day]) days[d++] = day;
				}
				int firstHour = firstSlot / 12;
				int maxFirstHour = (int)(CONSTANTS.eventStartDefault() / 12);
				int minLastHour = (int)((11 + CONSTANTS.eventStopDefault()) / 12);
				int lastHour = 1 + (lastSlot - 1) / 12;
				if (firstHour <= maxFirstHour && firstHour > 0 && ((firstSlot % 12) < 6)) firstHour--;
				HashMap<Long, String> colors = new HashMap<Long, String>();
				
				final TimeGrid tg = new TimeGrid(colors, days, (int)(1000 / nrDays), 55, true, false, (firstHour < maxFirstHour ? firstHour : maxFirstHour), (lastHour > minLastHour ? lastHour : minLastHour), EventResourceTimetable.this);
				tg.setShowRoomNote(true);
				tg.setResourceType(getResourceType());
				tg.setSelectedWeeks(iWeekPanel.getSelected());
				tg.setRoomResources(iRoomPanel.getSelected());
				tg.setMode(gridMode());
				tg.showVerticalSplit();

				for (EventInterface event: sortedEvents()) {
					List<MeetingInterface> meetings = new ArrayList<MeetingInterface>();
					for (MeetingInterface meeting: event.getMeetings()) {
						if (meeting.getApprovalStatus() != ApprovalStatus.Pending && meeting.getApprovalStatus() != ApprovalStatus.Approved) continue;
						if (meeting.getMeetingDate() != null && !filterEvent(event, meeting))
							meetings.add(meeting);
					}
					if (!meetings.isEmpty())
						tg.addEvent(event, meetings);
				}
				if (iWeekPanel.getValue() != null)
					tg.labelDays(iWeekPanel.getValue().getFirst(), iWeekPanel.getValue().getLast());
				
				// Move header row to thead
				Element headerRow = table.getRowFormatter().getElement(0);
				Element tableElement = table.getElement();
				Element thead = DOM.createTHead();
				tableElement.insertFirst(thead);
				headerRow.getParentElement().removeChild(headerRow);
				thead.appendChild(headerRow);
				
				ToolBox.print(
						new ToolBox.Page() {
							@Override
							public String getName() { return iHeader.getHeaderTitle(); }
							@Override
							public String getUser() { return iRoomPanel.getValue().toString(); }
							@Override
							public String getSession() { return iWeekPanel.getValue().toString(); }
							@Override
							public Element getBody() { return tg.getElement(); }
						},
						new ToolBox.Page() {
							@Override
							public String getName() { return iHeader.getHeaderTitle(); }
							@Override
							public String getUser() { return iRoomPanel.getValue().toString(); }
							@Override
							public String getSession() { return iWeekPanel.getValue().toString(); }
							@Override
							public Element getBody() { return table.getElement(); }
						}
						);
			}
		});

		iHeader.addButton("export", MESSAGES.buttonExport(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent clickEvent) {
				if (iProperties != null && iProperties.isCanExportCSV()) {
					final PopupPanel popup = new PopupPanel(true);
					MenuBar menu = new MenuBar(true);
					MenuItem exportPdf = new MenuItem(MESSAGES.opExportPDF(), true, new Command() {
						@Override
						public void execute() {
							popup.hide();
							export("output=" + (getSelectedTab() <= 1 ? "events" : "meetings") + ".pdf&flags=" + EventCookie.getInstance().getFlags(), true);
						}
					});
					exportPdf.getElement().getStyle().setCursor(Cursor.POINTER);
					menu.addItem(exportPdf);
					MenuItem exportCsv = new MenuItem(MESSAGES.opExportCSV(), true, new Command() {
						@Override
						public void execute() {
							popup.hide();
							export("output=" + (getSelectedTab() <= 1 ? "events" : "meetings") + ".csv&flags=" + EventCookie.getInstance().getFlags(), true);
						}
					});
					exportCsv.getElement().getStyle().setCursor(Cursor.POINTER);
					menu.addItem(exportCsv);
					MenuItem exportXls = new MenuItem(MESSAGES.opExportXLS(), true, new Command() {
						@Override
						public void execute() {
							popup.hide();
							export("output=" + (getSelectedTab() <= 1 ? "events" : "meetings") + ".xls&flags=" + EventCookie.getInstance().getFlags(), true);
						}
					});
					exportXls.getElement().getStyle().setCursor(Cursor.POINTER);
					menu.addItem(exportXls);
					MenuItem exportIcs = new MenuItem(MESSAGES.opExportICalendar(), true, new Command() {
						@Override
						public void execute() {
							popup.hide();
							copyToClipboard("output=events.ics" + (iType == PageType.Personal ? "&e:flag=All+Sessions" : ""), iType == PageType.Personal);
						}
					});
					exportIcs.getElement().getStyle().setCursor(Cursor.POINTER);
					menu.addItem(exportIcs);
					popup.add(menu);
					popup.showRelativeTo((UIObject)clickEvent.getSource());
					menu.focus();
				} else {
					export("output=events.ics", false);
				}
			}
		});
		iHeader.addButton("operations", MESSAGES.buttonMoreOperations(), 75, new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				final PopupPanel popup = new PopupPanel(true);
				iTable.getHeader(0).setMenu(popup);
				popup.showRelativeTo((UIObject)event.getSource());
				((MenuBar)popup.getWidget()).focus();
			}
		});
		iHeader.setEnabled("print", false);
		iHeader.setEnabled("export", false);
		iHeader.setEnabled("operations", false);
		
		iSession.addAcademicSessionChangeHandler(new AcademicSessionChangeHandler() {
			@Override
			public void onAcademicSessionChange(AcademicSessionChangeEvent event) {
				loadProperties(new AsyncCallback<EventPropertiesRpcResponse>() {
					@Override
					public void onFailure(Throwable caught) {
					}
					@Override
					public void onSuccess(EventPropertiesRpcResponse result) {
						if (!iInitialized)
							setup(true);
						else
							resourceTypeChanged(isShowingResults());
					}
				});
			}
		});
		
		iEventDetail = new EventDetail(this) {
			@Override
			protected void onHide() {
				iRootPanel.setWidget(iPanel);
				UniTimePageLabel.getInstance().setPageName(getPageName());
				if (!isShowingResults())
					resourceTypeChanged(true);
				changeUrl();
			}
			@Override
			protected void onShow() {
				iBack.clear();
				if (iTable != null) iTable.clearHover();
				iRootPanel.setWidget(iEventDetail);
				changeUrl();
			}
			@Override
			protected void edit() {
				super.hide();
				iEventAdd.setEvent(getEvent());
				iEventAdd.show();
			}
			@Override
			protected EventInterface getPrevious(Long eventId) {
				return iTable.previous(eventId);
			}
			@Override
			protected EventInterface getNext(Long eventId) {
				return iTable.next(eventId);
			}
			@Override
			protected void previous(final EventInterface event) {
				iBack.add(getEvent());
				LoadingWidget.execute(EventDetailRpcRequest.requestEventDetails(iSession.getAcademicSessionId(), event.getId()), new AsyncCallback<EventInterface>() {
					@Override
					public void onFailure(Throwable caught) {
						UniTimeNotifications.error(MESSAGES.failedLoad(event.getName(), caught.getMessage()), caught);
					}
					@Override
					public void onSuccess(EventInterface result) {
						LoadingWidget.getInstance().hide();
						setEvent(result);
						changeUrl();
					}
				}, MESSAGES.waitLoading(event.getName()));
			}
			@Override
			protected void next(final EventInterface event) {
				previous(event);
			}
			@Override
			public void hide() {
				if (!iBack.isEmpty()) {
					EventInterface last = iBack.remove(iBack.size() - 1);
					setEvent(last);
				} else {
					super.hide();
				}
			}
			@Override
			protected void onApprovalOrReject(Long eventId, EventInterface event) {
				if (iData != null)
					populate(tinker(new GwtRpcResponseList<EventInterface>(iData), eventId, event));
			}
		};
		
		iEventAdd = new EventAdd(iSession, this) {
			@Override
			protected void onHide() {
				iSession.setFilter(EventResourceTimetable.this);
				iFilter.setWidget(iSessionRow, 1, iSession);
				final EventInterface modified = iEventAdd.getEvent(), detail = iEventDetail.getEvent(), saved = iEventAdd.getSavedEvent();
				if (saved != null) {
					if (iData != null)
						populate(tinker(new GwtRpcResponseList<EventInterface>(iData), (saved.getId() == null ? modified.getId() : saved.getId()), saved));
					if (saved.getId() != null) {
						iEventDetail.setEvent(saved);
						iEventDetail.show();
					} else {
						iRootPanel.setWidget(iPanel);
						UniTimePageLabel.getInstance().setPageName(getPageName());
					}
				} else if (modified != null && detail != null && detail.getId().equals(modified.getId())) {
					LoadingWidget.execute(EventDetailRpcRequest.requestEventDetails(iSession.getAcademicSessionId(), modified.getId()), new AsyncCallback<EventInterface>() {
						@Override
						public void onFailure(Throwable caught) {
							UniTimeNotifications.error(MESSAGES.failedLoad(detail.getName(), caught.getMessage()), caught);
						}
						@Override
						public void onSuccess(EventInterface result) {
							LoadingWidget.getInstance().hide();
							if (result == null) {
								iRootPanel.setWidget(iPanel);
								UniTimePageLabel.getInstance().setPageName(getPageName());
							} else {
								iEventDetail.setEvent(result);
								iEventDetail.show();
							}
						}
					}, MESSAGES.waitLoading(detail.getName()));
				} else {
					iRootPanel.setWidget(iPanel);
					UniTimePageLabel.getInstance().setPageName(getPageName());
				}
				changeUrl();
			}
			@Override
			protected void onShow() {
				if (iTable != null) iTable.clearHover();
				iRootPanel.setWidget(iEventAdd);
				changeUrl();
			}
		};
		
		iMeetingClickHandler = new MeetingClickHandler() {
			@Override
			public void onMeetingClick(final MeetingClickEvent event) {
				if (!event.getEvent().isCanView()) return;
				LoadingWidget.execute(EventDetailRpcRequest.requestEventDetails(iSession.getAcademicSessionId(), event.getEvent().getId()), new AsyncCallback<EventInterface>() {
					@Override
					public void onFailure(Throwable caught) {
						UniTimeNotifications.error(MESSAGES.failedLoad(event.getEvent().getName(), caught.getMessage()), caught);
					}
					@Override
					public void onSuccess(EventInterface result) {
						LoadingWidget.getInstance().hide();
						iEventDetail.setEvent(result);
						iEventDetail.show();
					}
				}, MESSAGES.waitLoading(event.getEvent().getName()));
			}
		};
		
		iTable = new EventMeetingTable(EventMeetingTable.Mode.ListOfEvents, true, this) {
			@Override
			protected void onSortByChanded(EventComparator.EventMeetingSortBy sortBy, boolean asc) {
				changeUrl();
			}
			@Override
			protected void onColumnShownOrHid(int eventCookieFlags) {
				changeUrl();
			}
		};
		iTable.setMeetingFilter(this);
		iTable.addMouseClickListener(new MouseClickListener<EventMeetingRow>() {
			@Override
			public void onMouseClick(final TableEvent<EventMeetingRow> event) {
				if (event.getData() == null) return;
				final EventInterface e = event.getData().getEvent();
				if (e == null || !e.isCanView()) return;
				LoadingWidget.execute(EventDetailRpcRequest.requestEventDetails(iSession.getAcademicSessionId(), e.getId()), new AsyncCallback<EventInterface>() {
					@Override
					public void onFailure(Throwable caught) {
						UniTimeNotifications.error(MESSAGES.failedLoad(e.getName(), caught.getMessage()), caught);
					}
					@Override
					public void onSuccess(EventInterface result) {
						LoadingWidget.getInstance().hide();
						iEventDetail.setEvent(result);
						iEventDetail.show();
					}
				}, MESSAGES.waitLoading(e.getName()));
			}
		});
		iTablePanel = new ScrollPanel(iTable);
		iTablePanel.addStyleName("scroll-panel");
		
		iApproveDialog = new ApproveDialog(this) {
			@Override
			protected void onSubmit(ApproveEventRpcRequest.Operation operation, List<EventMeetingRow> data, String message, boolean emailConfirmation) {
				Map<EventInterface, List<MeetingInterface>> event2meetings = new HashMap<EventInterface, List<MeetingInterface>>();
				List<EventInterface> events = new ArrayList<EventInterface>();
				for (EventMeetingRow row: data) {
					if (row.getMeeting() != null) {
						List<MeetingInterface> meetings = event2meetings.get(row.getEvent());
						if (meetings == null) {
							meetings = new ArrayList<EventInterface.MeetingInterface>();
							event2meetings.put(row.getEvent(), meetings);
							events.add(row.getEvent());
						}
						meetings.add(row.getMeeting());
					} else {
						events.add(row.getEvent());
					}
				}
				onSubmit(operation, events.iterator(), event2meetings, message, emailConfirmation, new GwtRpcResponseList<EventInterface>(iData));
			}
			
			protected void onSubmit(final ApproveEventRpcRequest.Operation operation, final Iterator<EventInterface> events, final Map<EventInterface, List<MeetingInterface>> event2meetings, final String message, final boolean emailConfirmation, final GwtRpcResponseList<EventInterface> data) {
				if (events.hasNext()) {
					final EventInterface event = events.next();
					List<MeetingInterface> meetings = event2meetings.get(event);
					if (meetings == null) {
						meetings = new ArrayList<MeetingInterface>();
						for (MeetingInterface meeting: event.getMeetings()) {
							switch(operation) {
							case APPROVE:
							case REJECT:
								if (meeting.isCanApprove() && !filter(event, meeting)) meetings.add(meeting);
								break;
							case CANCEL:
								if (meeting.isCanCancel() && !filter(event, meeting)) meetings.add(meeting);
								break;
							case INQUIRE:
								if (meeting.isCanInquire() && !filter(event, meeting)) meetings.add(meeting);
								break;
							}
						}
					}
					if (meetings.isEmpty()) {
                        onSubmit(operation, events, event2meetings, message, emailConfirmation, data);
					} else {
						switch (operation) {
						case APPROVE: LoadingWidget.getInstance().show(MESSAGES.waitForApproval(event.getName())); break;
						case INQUIRE: LoadingWidget.getInstance().show(MESSAGES.waitForInquiry(event.getName())); break;
						case REJECT: LoadingWidget.getInstance().show(MESSAGES.waitForRejection(event.getName())); break;
						case CANCEL: LoadingWidget.getInstance().show(MESSAGES.waitForCancellation(event.getName())); break;
						}
						RPC.execute(ApproveEventRpcRequest.createRequest(operation, iSession.getAcademicSessionId(), event, meetings, message, emailConfirmation), new AsyncCallback<SaveOrApproveEventRpcResponse>() {
							@Override
							public void onFailure(Throwable caught) {
								LoadingWidget.getInstance().hide();
								UniTimeNotifications.error(caught);
								onSubmit(operation, events, event2meetings, message, emailConfirmation, data);
							}
							@Override
							public void onSuccess(SaveOrApproveEventRpcResponse result) {
								LoadingWidget.getInstance().hide();
								if (result.hasMessages())
									for (MessageInterface m: result.getMessages()) {
										if (m.isError())
											UniTimeNotifications.warn(m.getMessage());
										else if (m.isWarning())
											UniTimeNotifications.error(m.getMessage());
										else
											UniTimeNotifications.info(m.getMessage());
									}
								switch (operation) {
								case APPROVE:
								case REJECT:
								case CANCEL:
									tinker(data, event.getId(), result.getEvent());
								}
								onSubmit(operation, events, event2meetings, message, emailConfirmation, data);
							}
						});
					}
				} else {
					LoadingWidget.getInstance().hide();
					populate(data);
				}
			}
		};
		iTable.setOperation(EventMeetingTable.OperationType.Approve, iApproveDialog);
		iTable.setOperation(EventMeetingTable.OperationType.Reject, iApproveDialog);
		iTable.setOperation(EventMeetingTable.OperationType.Inquire, iApproveDialog);
		iTable.setOperation(EventMeetingTable.OperationType.Cancel, iApproveDialog);
		
		History.addValueChangeHandler(new ValueChangeHandler<String>() {
			@Override
			public void onValueChange(ValueChangeEvent<String> event) {
				if (iInitialized) {
					if ("event=add".equals(event.getValue()))
						iHistoryToken.parse(event.getValue());
					else
						iHistoryToken.reset(event.getValue());
					setup(false);
					if (LoadingWidget.getInstance().isShowing())
						LoadingWidget.getInstance().hide();
				}
			}
		});
	}
	
	private int getSelectedTab() {
		return iTabBar.getSelectedTab();
	}
	
	private void tabOrDataChanged(boolean keepSelection) {
		if (iTimeGrid != null) iTimeGrid.hideSelectionPopup();
		if (iTable != null) iTable.clearHover();
		if (getSelectedTab() == 0) {
			int nrDays = 4;
			int firstSlot = 12 + CONSTANTS.eventStartDefault(), lastSlot = CONSTANTS.eventStopDefault();
			for (EventInterface event: iData) {
				if (event.getType() == EventType.Unavailabile && !iEvents.hasChip(new Chip("type", "Not Available"))) continue;
				for (MeetingInterface meeting: event.getMeetings()) {
					if (filterEvent(event, meeting)) continue;
					if (meeting.getApprovalStatus() != ApprovalStatus.Pending && meeting.getApprovalStatus() != ApprovalStatus.Approved) continue;
					if (firstSlot > meeting.getStartSlot()) firstSlot = meeting.getStartSlot();
					if (lastSlot < meeting.getEndSlot()) lastSlot = meeting.getEndSlot();
					int day = (meeting.getDayOfWeek() + getFirstDayOfWeek()) % 7;
					nrDays = Math.max(nrDays, day);
				}
			}
			Chip after = iEvents.getChip("after");
			if (after != null) {
				Integer slot = TimeUtils.parseTime(CONSTANTS, after.getValue(), null);
				if (slot != null && firstSlot > slot) firstSlot = slot;
			}
			Chip before = iEvents.getChip("before");
			if (before != null) {
				Integer slot = TimeUtils.parseTime(CONSTANTS, before.getValue(), Math.min(firstSlot, CONSTANTS.eventStartDefault()));
				if (slot != null && lastSlot < slot) lastSlot = slot;
			}
			nrDays ++;
			int days[] = new int[nrDays];
			for (int i = 0; i < days.length; i++)
				days[i] = (i + getFirstDayOfWeek()) % 7;
			int firstHour = firstSlot / 12;
			int lastHour = 1 + (lastSlot - 1) / 12;
			int maxFirstHour = (int)(CONSTANTS.eventStartDefault() / 12);
			int minLastHour = (int)((11 + CONSTANTS.eventStopDefault()) / 12);
			if (firstHour <= maxFirstHour && firstHour > 0 && ((firstSlot % 12) < 6)) firstHour--;
			HashMap<Long, String> colors = new HashMap<Long, String>();
			List<SelectionInterface> selections = null;

			if (iTimeGrid != null) {
				if (keepSelection) selections = iTimeGrid.getSelections();
				iTimeGrid.destroy();
			}
			iTimeGrid = new TimeGrid(colors, days, (int)(0.9 * ToolBox.getClientWidth() / nrDays), false, false, (firstHour < maxFirstHour ? firstHour : maxFirstHour), (lastHour > minLastHour ? lastHour : minLastHour), this);
			iTimeGrid.setShowRoomNote(true);
			iTimeGrid.addMeetingClickHandler(iMeetingClickHandler);

			iTimeGrid.clear();
			iTimeGrid.setResourceType(getResourceType());
			iTimeGrid.setSelectedWeeks(iWeekPanel.getSelected());
			iTimeGrid.setRoomResources(iRoomPanel.getSelected());
			if (selections != null) iTimeGrid.getSelections().addAll(selections);
			iTimeGrid.setMode(gridMode());
			int first = iWeekPanel.getFirstDayOfYear(), last = iWeekPanel.getLastDayOfYear();
			for (EventInterface event: sortedEvents()) {
				List<MeetingInterface> meetings = new ArrayList<MeetingInterface>();
				for (MeetingInterface meeting: event.getMeetings()) {
					if (meeting.getApprovalStatus() != ApprovalStatus.Pending && meeting.getApprovalStatus() != ApprovalStatus.Approved) continue;
					if (meeting.getMeetingDate() != null && !filterEvent(event, meeting) && first <= meeting.getDayOfYear() && meeting.getDayOfYear() <= last)
						meetings.add(meeting);
				}
				if (!meetings.isEmpty())
					iTimeGrid.addEvent(event, meetings);
			}
			iTimeGrid.shrink(iEvents.hasChip(new FilterBox.Chip("day", null)), iEvents.hasChip(new Chip("type", "Not Available")),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[0])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[1])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[2])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[3])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[4])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[5])),
					iEvents.hasChip(new FilterBox.Chip("day", CONSTANTS.longDays()[6])));
			List<WeekInterface> selected = iWeekPanel.getSelected();
			if (selected != null && !selected.isEmpty())
				iTimeGrid.labelDays(selected.get(0), selected.size() == 1 ? null : selected.get(selected.size() - 1));
			iGridOrTablePanel.setWidget(iTimeGrid);
		} else {
			iTable.setMode(getSelectedTab() == 1 ? EventMeetingTable.Mode.ListOfEvents : EventMeetingTable.Mode.ListOfMeetings);
			iTable.setEvents(iData);
			iGridOrTablePanel.setWidget(iTablePanel);
		}
		iHeader.setHeaderTitle(name(getSelectedTab()));
		changeUrl();
	}
	
	private void setup(boolean init) {
		if (init)
			iInitialized = true;
		boolean reload = init;
		boolean isDefault = true;
		if (iHistoryToken.isChanged("events", "", iEvents.getValue())) {
			iEvents.setValue(iHistoryToken.getParameter("events", ""), true);
			reload = true;
			if (!iEvents.getValue().equals("")) isDefault = false;
		}
		if (iHistoryToken.isChanged("type", "room", iResourceTypes.getValue(iResourceTypes.getSelectedIndex()).toLowerCase())) {
			String typeString = iHistoryToken.getParameter("type", "room");
			if (typeString != null)
				for (int idx = 0; idx < iResourceTypes.getItemCount(); idx ++) {
					if (iResourceTypes.getValue(idx).equalsIgnoreCase(typeString)) {
						iResourceTypes.setSelectedIndex(idx);
					}
				}
			reload = true;
		}
		if (iHistoryToken.isChanged("rooms", (getResourceType() == ResourceType.ROOM ? "flag:Event" : ""), iRooms.getValue())) {
			iRooms.setValue(iHistoryToken.getParameter("rooms", (getResourceType() == ResourceType.ROOM ? "flag:Event" : "")), true);
			reload = true;
			if (!iRooms.getValue().equals(getResourceType() == ResourceType.ROOM ? "flag:Event" : "")) {
				isDefault = false;
			}
		}
		if (iHistoryToken.isChanged("name", "", iResources.getText())) {
			iResources.setText(iHistoryToken.getParameter("name", ""));
			reload = (getResourceType() != ResourceType.ROOM);
			if (!iResources.getText().equals("")) isDefault = false;
		}
		UniTimePageLabel.getInstance().setPageName(getPageName());
		if (iHistoryToken.isChanged("tab", "0", String.valueOf(getSelectedTab()))) {
			iTabBar.selectTab(Integer.parseInt(iHistoryToken.getParameter("tab", "0")), isShowingResults());
		}
		boolean fireWeekPanel = false, fireRoomPanel = false;
		if (iHistoryToken.isChanged("date", iLocDate)) {
			iLocDate = iHistoryToken.getParameter("date");
			if (iWeekPanel.hasValues()) {
				iWeekPanel.setValue(iWeekPanel.parse(iLocDate));
				iWeekPanel.setFilterEnabled(!iMatchingWeeks.isEmpty());
				fireWeekPanel = isShowingResults();
			}
		}
		if (iHistoryToken.isChanged("room", iLocRoom)) {
			iLocRoom = iHistoryToken.getParameter("room");
			if (iRoomPanel.hasValues()) {
				iRoomPanel.setFilterEnabled(!iMatchingRooms.isEmpty());
				iRoomPanel.setValue(iRoomPanel.parse(iLocRoom));
				fireRoomPanel = isShowingResults();
			}
		}
		if (iHistoryToken.hasParameter("sort"))
			iTable.setSortBy(iHistoryToken.getParameter("sort"));
		if (iHistoryToken.hasParameter("event")) {
			if (iHistoryToken.isChanged("term", iSession.getAcademicSessionAbbreviation()) && iHistoryToken.getParameter("term") != null)
				iSession.selectSession(iHistoryToken.getParameter("term"), null);
			if ("add".equals(iHistoryToken.getParameter("event"))) {
				iEventAdd.setEvent(null);
				iEventAdd.show();
			} else {
				new Timer() {
					@Override
					public void run() {
						if (iProperties == null) {
							schedule(500); return;
						}
						Long eventId = Long.valueOf(iHistoryToken.getParameter("event"));
						LoadingWidget.execute(EventDetailRpcRequest.requestEventDetails(iSession.getAcademicSessionId(), eventId), new AsyncCallback<EventInterface>() {
							@Override
							public void onFailure(Throwable caught) {
								UniTimeNotifications.error(MESSAGES.failedLoad(MESSAGES.anEvent(), caught.getMessage()), caught);
							}
							@Override
							public void onSuccess(EventInterface result) {
								LoadingWidget.getInstance().hide();
								iEventDetail.setEvent(result);
								iEventDetail.show();
							}
						}, MESSAGES.waitLoading(MESSAGES.anEvent()));		
					}
				}.schedule(200);
			}
		} else {
			iRootPanel.setWidget(iPanel);
			if (iHistoryToken.hasParameter("term") && iHistoryToken.isChanged("term", iSession.getAcademicSessionAbbreviation())) {
				iSession.selectSession(iHistoryToken.getParameter("term"), null);
			} else if (reload && iProperties != null) {
				resourceTypeChanged(isShowingResults() || (!isDefault && CONSTANTS.searchWhenPageIsLoaded()));
			}
		}
		if (fireWeekPanel)
			ValueChangeEvent.fire(iWeekPanel, iWeekPanel.getValue());
		if (fireRoomPanel)
			ValueChangeEvent.fire(iRoomPanel, iRoomPanel.getValue());
	}
	
	private Collection<EventInterface> sortedEvents() {
		TreeSet<EventInterface> data = new TreeSet<EventInterface>(new Comparator<EventInterface>() {
			@Override
			public int compare(EventInterface e1, EventInterface e2) {
				int cmp = Integer.valueOf(e1.getMeetings().first().getDayOfYear()).compareTo(e2.getMeetings().first().getDayOfYear());
				if (cmp != 0) return cmp;
				cmp = Integer.valueOf(e1.getMeetings().first().getStartSlot()).compareTo(e2.getMeetings().first().getStartSlot());
				if (cmp != 0) return cmp;
				return e1.compareTo(e2);
			}
		});
		data.addAll(iData);
		return data;
	}
	
	public ResourceType getResourceType() {
		if (iResourceTypes.getSelectedIndex() < 0)
			return null;
		return ResourceType.valueOf(iResourceTypes.getValue(iResourceTypes.getSelectedIndex()));
	}
	
	public String getResourceName() {
		if (getResourceType() == ResourceType.PERSON && (iProperties == null || !iProperties.isCanLookupPeople())) return "";
		return (iResources.getText() == null || iResources.getText().isEmpty() ? null : iResources.getText());
	}
	
	private void resourceTypeChanged(boolean loadData) {
		ResourceType type = getResourceType();
		if (type == null) {
			UniTimeNotifications.warn(MESSAGES.warnNoResourceType());
			return;
		}
		if (type == ResourceType.ROOM) {
			iFilter.getRowFormatter().setVisible(iResourcesRow, false);
			iFilterHeader.setEnabled("lookup", false);
			if (loadData) {
				if (iSession.getAcademicSessionId() == null) {
					UniTimeNotifications.warn(MESSAGES.warnNoSession());
				} else if (iProperties == null) {
					UniTimeNotifications.warn(MESSAGES.warnNoEventProperties(iSession.getAcademicSessionName()));
				} else {
					ResourceInterface resource = new ResourceInterface();
					resource.setType(ResourceType.ROOM);
					resource.setName(iRooms.getValue());
					resourceChanged(resource);
				}
			}
		} else {
			iFilter.getRowFormatter().setVisible(iResourcesRow, type != ResourceType.PERSON);
			((Label)iFilter.getWidget(iResourcesRow, 0)).setText(type.getName(CONSTANTS) + ":");
			iFilterHeader.setEnabled("lookup", iProperties != null && iProperties.isCanLookupPeople() && getResourceType() == ResourceType.PERSON);
			if (getResourceName() != null || (type == ResourceType.PERSON && loadData)) {
				if (iSession.getAcademicSessionId() == null) {
					UniTimeNotifications.warn(MESSAGES.warnNoSession());
				} else if (iProperties == null) {
					UniTimeNotifications.warn(MESSAGES.warnNoEventProperties(iSession.getAcademicSessionName()));
				} else {
					LoadingWidget.execute(ResourceLookupRpcRequest.findResource(iSession.getAcademicSessionId(), type, getResourceName()),
							new AsyncCallback<GwtRpcResponseList<ResourceInterface>>() {
						@Override
						public void onFailure(Throwable caught) {
							UniTimeNotifications.error(MESSAGES.failedLoad(getResourceName(), caught.getMessage()), caught);
							hideResults();
						}
						@Override
						public void onSuccess(GwtRpcResponseList<ResourceInterface> result) {
							resourceChanged(result.get(0));
						}
					}, MESSAGES.waitLoading(type.getName(CONSTANTS) + (type != ResourceType.PERSON ? " " + getResourceName() : "")));
				}
			} else if (getResourceName() == null && loadData) {
				UniTimeNotifications.warn(MESSAGES.warnNoResourceName(CONSTANTS.resourceName()[getResourceType().ordinal()].toLowerCase()));
				iResources.setFocus(true);
			}
		}
		if (!loadData) {
			iData = null;
			hideResults();
		}
	}
	
	private GwtRpcResponseList<EventInterface> tinker(GwtRpcResponseList<EventInterface> data, Long oldEventId, EventInterface newEvent) {
		for (Iterator<EventInterface> i = data.iterator(); i.hasNext(); ) {
			EventInterface event = i.next();

			if (event.getId().equals(oldEventId)) {
				i.remove(); continue;
			} else if (event.hasConflicts()) {
				for (Iterator<EventInterface> j = event.getConflicts().iterator(); j.hasNext(); )
					if (j.next().getId().equals(oldEventId)) j.remove();
			}
			
			if (newEvent != null && newEvent.getId() != null && event.inConflict(newEvent)) {
				event.addConflict(event.createConflictingEvent(newEvent));
			}
		}
		
		if (newEvent != null && newEvent.getId() != null)
			data.add(newEvent);
		
		return data;
	}
	
	private void resourceChanged(final ResourceInterface resource) {
		iResource = resource;
		LoadingWidget.execute(iRooms.getElementsRequest(), new AsyncCallback<FilterRpcResponse>() {
			@Override
			public void onFailure(Throwable caught) {
				UniTimeNotifications.error(MESSAGES.failedLoad(resource.getType() == ResourceType.ROOM ? MESSAGES.resourceRoom().toLowerCase() : resource.getName(), caught.getMessage()), caught);
				hideResults();
			}

			@Override
			public void onSuccess(FilterRpcResponse result) {
				if (result == null) return;
				List<ResourceInterface> rooms = new ArrayList<ResourceInterface>();
				if (result.hasResults())
					for (FilterRpcResponse.Entity room: result.getResults()) {
						rooms.add(new ResourceInterface(room));
					}
				Collections.sort(rooms);
				iRoomPanel.setValues(rooms);
				
				if (!result.hasResults() || result.getResults().isEmpty()) {
					UniTimeNotifications.error(MESSAGES.errorNoMatchingRooms());
					hideResults();
					return;
				}
				
				LoadingWidget.execute(EventLookupRpcRequest.findEvents(iSession.getAcademicSessionId(), iResource, iEvents.getElementsRequest(), iRooms.getElementsRequest(), CONSTANTS.maxMeetings()), 
						new AsyncCallback<GwtRpcResponseList<EventInterface>>() {
					@Override
					public void onSuccess(GwtRpcResponseList<EventInterface> result) {
						if (result != null) populate(result);
						iApproveDialog.reset(iProperties);
					}
			
					@Override
					public void onFailure(Throwable caught) {
						UniTimeNotifications.error(MESSAGES.failedLoad(resource.getType() == ResourceType.ROOM ? MESSAGES.resourceRoom().toLowerCase() : resource.getName(), caught.getMessage()), caught);
						hideResults();
					}
				}, MESSAGES.waitLoadingTimetable(resource.getType() == ResourceType.ROOM ? MESSAGES.resourceRoom().toLowerCase() : resource.getName(), iSession.getAcademicSessionName()));
			}
			
		}, MESSAGES.waitLoadingTimetable(resource.getType() == ResourceType.ROOM ? MESSAGES.resourceRoom().toLowerCase() : resource.getName(), iSession.getAcademicSessionName()));
	}
	
	private void populate(GwtRpcResponseList<EventInterface> result) {
		iData = result;
		if (iData.isEmpty())
			UniTimeNotifications.warn(MESSAGES.failedNoEvents());
		
		Collections.sort(iData);
		
		iMatchingRooms.clear();
		int nrMeetings = 0;
		for (EventInterface event: iData) {
			for (MeetingInterface meeting: event.getMeetings()) {
				if (meeting.getLocation() != null) iMatchingRooms.add(meeting.getLocation().getId());
			}
			nrMeetings += event.getMeetings().size();
		}
		iRoomPanel.setValue(iRoomPanel.parse(iLocRoom));
		iRoomPanel.setFilterEnabled(!iMatchingRooms.isEmpty());

		iMatchingWeeks.clear();
		for (WeekInterface week: iWeekPanel.getValues()) {
			boolean hasEvents = false;
			events: for (EventInterface event: iData) {
				for (MeetingInterface meeting: event.getMeetings()) {
					if (meeting.getDayOfYear() >= week.getDayOfYear() && meeting.getDayOfYear() < week.getDayOfYear() + 7) {
						hasEvents = true;
						break events;
					}
				}
			}
			if (hasEvents) iMatchingWeeks.add(week.getDayOfYear());
		}
		iWeekPanel.setValue(iWeekPanel.parse(iLocDate));
		iWeekPanel.setFilterEnabled(!iMatchingWeeks.isEmpty());

		if (nrMeetings > CONSTANTS.maxMeetings())
			iHeader.setErrorMessage(MESSAGES.warnTooManyMeetings(CONSTANTS.maxMeetings()));
		else
			iHeader.clearMessage();
		
		tabOrDataChanged(false);
							
		showResults();
		
		changeUrl();
	}
	
	private String name(int tab) {
		String resource;
		if (iResource.getType() == ResourceType.ROOM) {
			if (iRoomPanel.getValue() != null && iRoomPanel.getValue().isOne()) {
				resource = iRoomPanel.getValue().getFirst().getName();
			} else if (iRoomPanel.getValues() != null && iRoomPanel.getValues().size() == 1) {
				resource = iRoomPanel.getValues().get(0).getName();
			} else {
				resource = MESSAGES.resourceRoom();
			}
		} else {
			resource = iResource.getNameWithHint(MESSAGES);
		}
		String session = iSession.getAcademicSessionName();
		if (iWeekPanel.getValue() != null && !iWeekPanel.getValue().isAll())
			session = iWeekPanel.getValue().toString().toLowerCase();
		switch (tab) {
		case 0: return MESSAGES.sectTimetable(resource, session);
		case 1: return MESSAGES.sectEventList(resource, session);
		case 2: return MESSAGES.sectMeetingList(resource, session);
		}
		return null;
	}
	
	@Override
	public boolean filter(EventInterface event, MeetingInterface meeting) {
		// if (event != null && event.getId() == null) return true;
		if (event != null && event.getType() == EventType.Unavailabile && !iEvents.hasChip(new Chip("type", "Not Available"))) return true;
		return filterEvent(event, meeting);
	}
	
	public boolean filterEvent(EventInterface event, MeetingInterface meeting) {
		if (event != null && event.getType() == EventType.Unavailabile && !iEvents.hasChip(new Chip("type", "Not Available"))) {
			if (getResourceType() != ResourceType.ROOM || gridMode() != TimeGrid.Mode.OVERLAP) return true;
		}
		if (iWeekPanel.getValue() != null && !iWeekPanel.getValue().isAll()) {
			int firstDayOfYear = iWeekPanel.getValue().getFirst().getDayOfYear();
			int lastDayOfYear = (iWeekPanel.getValue().isOne() ? iWeekPanel.getValue().getFirst() : iWeekPanel.getValue().getLast()).getDayOfYear() + 6;
			if (meeting.getMeetingDate() == null || meeting.getDayOfYear() < firstDayOfYear || meeting.getDayOfYear() > lastDayOfYear)
				return true;
		}
		if (iRoomPanel.getValue() != null && !iRoomPanel.getValue().isAll()) {
			if (iRoomPanel.getValue().isOne()) {
				if (!iRoomPanel.getValue().getFirst().getName().equals(meeting.getLocationName()) &&
					!iRoomPanel.getValue().getFirst().sameLocationOrPartition(meeting.getLocation())) return true;
			} else {
				if (iRoomPanel.getValue().getFirst().getName().compareTo(meeting.getLocationName()) > 0) return true;
				if (iRoomPanel.getValue().getLast().getName().compareTo(meeting.getLocationName()) < 0) return true;
			}
		}
		
		return false;
	}
	
	private TimeGrid.Mode gridMode() {
		if ("0".equals(iHistoryToken.getParameter("eq"))) return TimeGrid.Mode.FILLSPACE;
		switch (iResource.getType()) {
		case ROOM:
			return (iRoomPanel.isOne() || (iWeekPanel.isOne() && iRoomPanel.getSelected().size() <= 20) ? TimeGrid.Mode.OVERLAP : TimeGrid.Mode.PROPORTIONAL);
		case PERSON:
			return TimeGrid.Mode.OVERLAP;
		case GROUP:
			if (CONSTANTS.timeGridStudentGroupDoesNotOverlap()) return TimeGrid.Mode.OVERLAP;
			// else use default
		default:
			return (iRoomPanel.isOne() || (iWeekPanel.isOne() && iRoomPanel.getSelected().size() <= 10) ? TimeGrid.Mode.OVERLAP : TimeGrid.Mode.PROPORTIONAL);
		}
	}
		
	public static class ResourceSuggestion implements Suggestion {
		private ResourceInterface iResource;
		
		public ResourceSuggestion(ResourceInterface resource) {
			iResource = resource;
		}
		
		public ResourceInterface getResource() {
			return iResource;
		}

		@Override
		public String getDisplayString() {
			if (iResource.hasTitle())
				return iResource.getTitle();
			if (iResource.hasAbbreviation() && !iResource.getAbbreviation().equals(iResource.getName()))
				return iResource.getAbbreviation() + " - " + iResource.getName();
			return (iResource.hasAbbreviation() ? iResource.getAbbreviation() : iResource.getName());
		}

		@Override
		public String getReplacementString() {
			return (iResource.hasAbbreviation() ? iResource.getAbbreviation() : iResource.getName());
		}
	}
	
	protected String query(String extra, boolean includeRoomWeekFilter) {
		String query = "sid=" + iSession.getAcademicSessionId() +
				(iResource == null || iResource.getType() == null ? "" : "&type=" + iResource.getType().toString().toLowerCase()) +
				(iResource == null || iResource.getId() == null ? "" : "&id=" + iResource.getId()) +
				(iResource == null || iResource.getExternalId() == null ? "" : "&ext=" + iResource.getExternalId());
		
		FilterRpcRequest events = iEvents.getElementsRequest();
		if (includeRoomWeekFilter) {
			if (iWeekPanel.getValue() != null && !iWeekPanel.getValue().isAll()) {
				events.setOption("from", String.valueOf(iWeekPanel.getValue().getFirst().getDayOfYear()));
				events.setOption("to", String.valueOf((iWeekPanel.getValue().isOne() ? iWeekPanel.getValue().getFirst() : iWeekPanel.getValue().getLast()).getDayOfYear() + 6));
			}
			if (iRoomPanel.getValue() != null && !iRoomPanel.getValue().isAll()) {
				for (ResourceInterface resource: iRoomPanel.getSelected())
					events.addOption("room", resource.getId().toString());
			} else {
				FilterRpcRequest rooms = iRooms.getElementsRequest();
				if (rooms.hasOptions()) {
					for (Map.Entry<String, Set<String>> option: rooms.getOptions().entrySet()) {
						for (String value: option.getValue()) {
							query += "&r:" + option.getKey() + "=" + URL.encodeQueryString(value);
						}
					}
				}
				if (rooms.getText() != null && !rooms.getText().isEmpty()) {
					query += "&r:text=" + URL.encodeQueryString(rooms.getText());
				}
			}
		} else {
			FilterRpcRequest rooms = iRooms.getElementsRequest();
			if (rooms.hasOptions()) {
				for (Map.Entry<String, Set<String>> option: rooms.getOptions().entrySet()) {
					for (String value: option.getValue()) {
						query += "&r:" + option.getKey() + "=" + URL.encodeQueryString(value);
					}
				}
			}
			if (rooms.getText() != null && !rooms.getText().isEmpty()) {
				query += "&r:text=" + URL.encodeQueryString(rooms.getText());
			}
		}
		if (events.hasOptions()) {
			for (Map.Entry<String, Set<String>> option: events.getOptions().entrySet()) {
				for (String value: option.getValue()) {
					query += "&e:" + option.getKey() + "=" + URL.encodeQueryString(value);
				}
			}
		}
		if (events.getText() != null && !events.getText().isEmpty()) {
			query += "&e:text=" + URL.encodeQueryString(events.getText());
		}
		
		if (iTable.hasSortBy())
			query += "&sort=" + iTable.getSortBy();
		
		if (extra != null && !extra.isEmpty()) query += "&" + extra;
		
		if (iEvents.hasChip(new Chip("type", "Not Available")) || (getResourceType() == ResourceType.ROOM && gridMode() == TimeGrid.Mode.OVERLAP))
			query += "&ua=1";
		
		return query;
	}
	
	protected void export(String format, boolean includeRoomWeekFilter) {
		RPC.execute(EncodeQueryRpcRequest.encode(query(format, includeRoomWeekFilter)), new AsyncCallback<EncodeQueryRpcResponse>() {
			@Override
			public void onFailure(Throwable caught) {
			}
			@Override
			public void onSuccess(EncodeQueryRpcResponse result) {
				ToolBox.open(GWT.getHostPageBaseURL() + "export?q=" + result.getQuery());
			}
		});
	}
	
	protected void copyToClipboard(String format, final boolean personal) {
		RPC.execute(EncodeQueryRpcRequest.encode(query(format, false), true), new AsyncCallback<EncodeQueryRpcResponse>() {
			@Override
			public void onFailure(Throwable caught) {
			}
			@Override
			public void onSuccess(final EncodeQueryRpcResponse result) {
				final UniTimeDialogBox dialog = new UniTimeDialogBox(true, false);
				dialog.setText(MESSAGES.opExportICalendar());
				dialog.setEscapeToHide(true);
				SimpleForm form = new SimpleForm();
				HTML description = null;
				if (personal)
					description = new HTML(MESSAGES.exportICalendarDescriptionPersonal(), true);
				else
					description = new HTML(MESSAGES.exportICalendarDescriptionOther(iSession.getAcademicSessionName()), true);
				description.getElement().getStyle().setProperty("max-width", "500px");
				form.addRow(description);
				final TextArea ta = new TextArea();
				ta.setStyleName("unitime-TextArea");
				ta.setVisibleLines(5);
				ta.setCharacterWidth(80);
				ta.setText(GWT.getHostPageBaseURL() + (result.hasHash() ? "export?x=" + result.getHash() : "export?q=" + result.getQuery()));
				UniTimeWidget<TextArea> w = new UniTimeWidget<TextArea>(ta);
				w.setHint(MESSAGES.hintCtrlCToCopy());
				form.addRow(w);
				w.getElement().getStyle().setMarginBottom(5, Unit.PX);
				
				HTML message = new HTML(MESSAGES.exportICalendarDownload(MESSAGES.buttonDownload()), true);
				message.getElement().getStyle().setProperty("max-width", "500px");
				form.addRow(message);
				
				UniTimeHeaderPanel h = new UniTimeHeaderPanel();
				h.addButton("download", MESSAGES.buttonDownload(), new ClickHandler() {
					@Override
					public void onClick(ClickEvent event) {
						ToolBox.open(GWT.getHostPageBaseURL() + "export?q=" + result.getQuery());
						dialog.hide();
					}
				});
				h.addButton("close", MESSAGES.buttonClose(), new ClickHandler() {
					@Override
					public void onClick(ClickEvent event) {
						dialog.hide();
					}
				});
				
				form.addBottomRow(h);
				dialog.setWidget(form);
				Scheduler.get().scheduleDeferred(new Command() {
					@Override
					public void execute() {
						ta.setFocus(true);
						ta.selectAll();
					}
				});
				dialog.center();
			}
		});
	}
	
	private native static void nativeCopyToClipboard(Element e)/*-{
		alert('here');
		e.focus();
		e.select();
		$doc.execCommand('Copy');
		alert('Copied');
	}-*/;
		
	protected void changeUrl() {
		if (iTimeGrid != null) {
			RPC.execute(EncodeQueryRpcRequest.encode(query("output=events.ics", true)), new AsyncCallback<EncodeQueryRpcResponse>() {
				@Override
				public void onFailure(Throwable caught) {
					iHeader.setEnabled("export", false);
					iHeader.setEnabled("operations", false);
				}
				@Override
				public void onSuccess(EncodeQueryRpcResponse result) {
					iHeader.setEnabled("export", true);
					iHeader.setEnabled("operations", getSelectedTab() > 0);
				}
			});
		}
		
		iHistoryToken.reset(null);
		iHistoryToken.setParameter("term", iSession.getAcademicSessionAbbreviation());
		iHistoryToken.setParameter("type", iResourceTypes.getValue(iResourceTypes.getSelectedIndex()).toLowerCase());
		if (iResource != null && iResource.getAbbreviation() != null && iResource.getType() != ResourceType.PERSON)
			 iHistoryToken.setParameter("name", iResource.getAbbreviation());
		if (iLocDate != null)
			iHistoryToken.setParameter("date", iLocDate);
		iHistoryToken.setParameter("events", iEvents.getValue().trim());
		iHistoryToken.setParameter("rooms", iRooms.getValue().trim());
		if (iLocRoom != null)
			iHistoryToken.setParameter("room", iLocRoom);
		iHistoryToken.setParameter("tab", String.valueOf(getSelectedTab()));
		if (iEventDetail.equals(iRootPanel.getWidget()))
			iHistoryToken.setParameter("event", iEventDetail.getEvent().getId());
		else if (iEventAdd.equals(iRootPanel.getWidget())) {
			Long id = iEventAdd.getEventId();
			iHistoryToken.setParameter("event", id == null ? "add" : id.toString());
		}
		if (iTable.hasSortBy())
			iHistoryToken.setParameter("sort", iTable.getSortBy());
		iHistoryToken.mark();
		Client.fireGwtPageChanged(new Client.GwtPageChangeEvent());
	}
	
	private class RoomSelector extends IntervalSelector<ResourceInterface> {
		public RoomSelector() {
			super(true);
			addValueChangeHandler(new ValueChangeHandler<Interval>() {
				@Override
				public void onValueChange(ValueChangeEvent<Interval> event) {
					setAriaLabel(ARIA.roomSelection(toAriaString()));
				}
			});
			
			addFocusHandler(new FocusHandler() {
				@Override
				public void onFocus(FocusEvent event) {
					setAriaLabel(ARIA.roomSelection(toAriaString()));
				}
			});
		}
		
		@Override
		public Interval parse(String query) {
			if (query == null || query.isEmpty() || getValues() == null) return new Interval();
			
			Interval ret = null;
			for (int i = 0; i < getValues().size(); i++) {
				ResourceInterface first = getValues().get(i); 
				if (query.equalsIgnoreCase(first.getName()))
					return new Interval(first);
				for (int j = i + 1; j < getValues().size(); j++) {
					ResourceInterface last = getValues().get(j);
					if (query.equalsIgnoreCase(first.getName() + "-" + last.getName()) || query.equalsIgnoreCase(first.getName() + " - " + last.getName()))
						return new Interval(first, last);
					if (filter(first) || filter(last)) continue;
					if (query.toLowerCase().startsWith(first.getName().toLowerCase()) && (
						(first.getName() + "-" + last.getName()).toLowerCase().startsWith(query.toLowerCase()) || (first.getName() + " - " + last.getName()).toLowerCase().startsWith(query.toLowerCase())))
						ret = new Interval(first, last);
					if (query.toLowerCase().startsWith(first.getName().toLowerCase()) && last.getName().indexOf(' ') >= 0) {
						String building = last.getName().substring(0, last.getName().lastIndexOf(' '));
						String room = last.getName().substring(last.getName().lastIndexOf(' ') + 1);
						if (first.getName().startsWith(building + " ") && ((first.getName() + "-" + room).toLowerCase().startsWith(query.toLowerCase()) || (first.getName() + " - " + room).toLowerCase().startsWith(query.toLowerCase())))
							ret = new Interval(first, last);
					}
				}
			}
			if (ret != null) return ret;

			ResourceInterface first = null, last = null;
			for (ResourceInterface e: getValues()) {
				if (isFilterEnabled() && filter(e)) continue;
				if (e.getName().equalsIgnoreCase(query)) return new Interval(e);
				if (e.getName().toLowerCase().startsWith(query.toLowerCase())) {
					if (first == null) first = e;
					last = e;
				} else {
					if (first != null) return new Interval(first, last);
				}
			}
			if (first != null) return new Interval(first, last);
			
			return new Interval();
		}
		
		@Override
		protected String getDisplayString(Interval interval) {
			if (interval == null || interval.isAll()) {
				return interval.isEnableFilter() ? MESSAGES.itemAllRoomsWithFilter() : MESSAGES.itemAllRooms();
			}
			if (interval.isOne()) {
				String hint = interval.getFirst().getRoomType() +
					(interval.getFirst().hasSize() ? ", " + MESSAGES.hintRoomCapacity("" + interval.getFirst().getSize()) : "") +
					(interval.getFirst().hasDistance() ? ", " + MESSAGES.hintRoomDistance("" + Math.round(interval.getFirst().getDistance())) : "");
				return interval.getFirst().getName() + " <span class='item-hint'>" + hint + "</span>";
			} else {
				return "&nbsp;&nbsp;&nbsp;" + interval.getFirst().getName() + " - " + interval.getLast().getName();
			}
		}
		
		@Override
		protected String getReplaceString(Interval interval) {
			if (interval == null || interval.isAll())
				return interval.isEnableFilter() ? MESSAGES.itemAllRoomsWithFilter() : MESSAGES.itemAllRooms();
			return interval.isOne() ? interval.getFirst().getName() : interval.getFirst().getName() + " - " + interval.getLast().getName();			

		}
		
		public String getSelection() {
			if (getValue() == null || getValue().isAll()) return "";
			return getValue().isOne() ? getValue().getFirst().getName() : getValue().getFirst().getName() + "-" + getValue().getLast().getName();			
		}
		
		@Override
		public void setValue(Interval value, boolean fireEvents) {
			if (value != null && ((value.getFirst() != null && filter(value.getFirst())) || (value.getLast() != null && filter(value.getLast()))))
				setFilterEnabled(false);
			super.setValue(value, fireEvents);
		}
	}
	
	private void hideResults() {
		for (int i = 1; i < iPanel.getRowCount(); i++)
			iPanel.getRowFormatter().setVisible(i, false);
		// iGridOrTablePanel.setVisible(false);
		// iTabBar.setVisible(false);
		iHeader.setEnabled("print", false);
		iHeader.setEnabled("export", false);
		iHeader.setEnabled("operations", false);
		if (iFooter != null) iFooter.clearMessage();
	}
	
	private void showResults() {
		for (int i = 1; i < iPanel.getRowCount(); i++)
			iPanel.getRowFormatter().setVisible(i, true);
		iHeader.setEnabled("print", true);
		iHeader.setEnabled("export", iTable.getRowCount() > 1);
		iHeader.setEnabled("operations", getSelectedTab() > 0 && iTable.getRowCount() > 1);
		// iGridOrTablePanel.setVisible(true);
		// iTabBar.setVisible(true);
		if (iFooter != null)  {
			iFooter.clearMessage();
			if (getResourceType() == ResourceType.PERSON && iData != null && iProperties != null && iProperties.isStudent()) {
				boolean multiRoom = false;
				data: for (EventInterface e: iData) {
					if (e.getType() == EventType.Class || e.getType() == EventType.FinalExam || e.getType() == EventType.MidtermExam) {
						MeetingInterface last = null;
						for (MeetingInterface m: e.getMeetings()) {
							if (last != null && last.overlapsWith(m) && last.hasLocation() && m.hasLocation() && !last.getLocation().equals(m.getLocation())) { multiRoom = true; break data; }
							last = m;
						}
						
					}
				}
				if (multiRoom) {
					iFooter.setMessage(MESSAGES.warnMultiRoomClassOrExam());
					// UniTimeNotifications.info(MESSAGES.warnMultiRoomClassOrExam());
				}
			}
		}
	}
	
	private boolean isShowingResults() {
		return iPanel.equals(iRootPanel.getWidget()) && iPanel.getRowFormatter().isVisible(1);
	}
	
	private void loadProperties(final AsyncCallback<EventPropertiesRpcResponse> callback) {
		iProperties = null;
		iSessionMonths = null;
		iTable.setShowMainContact(false);
		iTable.setShowMeetingContacts(false);
		iFilterHeader.setEnabled("lookup", false);
		iFilterHeader.setEnabled("add", false);
		iFooter.setEnabled("add", false);
		if (iSession.getAcademicSessionId() != null) {
			RPC.execute(EventPropertiesRpcRequest.requestEventProperties(iSession.getAcademicSessionId(), iType.name()), new AsyncCallback<EventPropertiesRpcResponse>() {
				@Override
				public void onFailure(Throwable caught) {
					UniTimeNotifications.error(MESSAGES.failedLoad(iSession.getAcademicSessionName(), caught.getMessage()), caught);
					if (callback != null)
						callback.onFailure(caught);
				}
				@Override
				public void onSuccess(final EventPropertiesRpcResponse result) {
					if (result.isCanSaveFilterDefaults() && "true".equals(iHistoryToken.getParameter("showFilter", "true"))) {
						iHistoryToken.setDefaultParameter("events", result.getFilterDefault("events"));
						iEvents.setDefaultValueProvider(new TakesValue<String>() {
							@Override
							public void setValue(final String value) {
								RPC.execute(new SaveFilterDefaultRpcRequest(iType.name() + ".events", iEvents.getValue()),
										new AsyncCallback<GwtRpcResponse>() {
											@Override
											public void onFailure(Throwable caught) {
												UniTimeNotifications.error(MESSAGES.failedSaveAsDefault(caught.getMessage()), caught);
											}
											@Override
											public void onSuccess(GwtRpcResponse result) {
												iHistoryToken.setDefaultParameter("events", value);
											}
										});					
							}

							@Override
							public String getValue() {
								return iHistoryToken.getDefaultParameter("events", "");
							}
						});
						iHistoryToken.setDefaultParameter("rooms", result.getFilterDefault("rooms"));
						iRooms.setDefaultValueProvider(new TakesValue<String>() {
							@Override
							public void setValue(final String value) {
								RPC.execute(new SaveFilterDefaultRpcRequest(iType.name() + ".rooms", iRooms.getValue()),
										new AsyncCallback<GwtRpcResponse>() {
											@Override
											public void onFailure(Throwable caught) {
												UniTimeNotifications.error(MESSAGES.failedSaveAsDefault(caught.getMessage()), caught);
											}
											@Override
											public void onSuccess(GwtRpcResponse result) {
												iHistoryToken.setDefaultParameter("rooms", value);
											}
										});					
							}

							@Override
							public String getValue() {
								return iHistoryToken.getDefaultParameter("rooms", "");
							}
						});
					}
					RPC.execute(new RequestSessionDetails(iSession.getAcademicSessionId()), new AsyncCallback<GwtRpcResponseList<SessionMonth>>() {
						@Override
						public void onFailure(Throwable caught) {
							UniTimeNotifications.error(MESSAGES.failedLoad(iSession.getAcademicSessionName(), caught.getMessage()), caught);
							if (callback != null)
								callback.onFailure(caught);
						}
						
						@Override
						public void onSuccess(GwtRpcResponseList<SessionMonth> months) {
							iProperties = result;
							iSessionMonths = months;
							iEvents.setOtherVisible(result.isCanLookupMainContact() || result.isCanLookupPeople());
							iFilterHeader.setEnabled("lookup", result.isCanLookupPeople() && getResourceType() == ResourceType.PERSON);
							iFilterHeader.setEnabled("add", result.isCanAddEvent() && "true".equals(iHistoryToken.getParameter("addEvent", "true")));
							iFooter.setEnabled("add", result.isCanAddEvent() && "true".equals(iHistoryToken.getParameter("addEvent", "true")));
							iEventAdd.setup(result);
							iTable.setShowMainContact(result.isCanLookupMainContact());
							iTable.setShowMeetingContacts(result.isCanViewMeetingContacts());
							iApproveDialog.reset(result);
							if (callback != null)
								callback.onSuccess(result);
						}
					});
				}
			});
			iLookup.setOptions("mustHaveExternalId,session=" + iSession.getAcademicSessionId());

		} else {
			iLookup.setOptions("mustHaveExternalId");
		}
	}
	
	@Override
	public Long getSessionId() {
		return iSession.getAcademicSessionId();
	}

	@Override
	public EventPropertiesRpcResponse getProperties() {
		return iProperties;
	}

	@Override
	public List<SelectionInterface> getSelection() {
		return (iTimeGrid == null ? null : iTimeGrid.getSelections());
	}

	@Override
	public String getRoomFilter() {
		return iRooms.getValue();
	}
	
	@Override
	public List<Date> getSelectedDates() {
		return null;
	}

	@Override
	public StartEndTime getSelectedTime() {
		return null;
	}

	@Override
	public ContactInterface getMainContact() {
		return iLookup.getValue() != null ? new EventInterface.ContactInterface(iLookup.getValue()) : iProperties == null ? null : iProperties.getMainContact();
	}
	
	@Override
	public boolean isTooEarly(int startSlot, int endSlot) {
		if (iProperties == null || !iProperties.hasTooEarlySlot()) return false;
		return (startSlot > 0 && startSlot <= iProperties.getTooEarlySlot()) || (startSlot == 0 && endSlot <= iProperties.getTooEarlySlot());
	}
	
	@Override
	public int getFirstDayOfWeek() {
		if (iProperties == null || iProperties.getFirstDayOfWeek() == null) return 0;
		return iProperties.getFirstDayOfWeek();
	}
	
	@Override
	public boolean isCanEmailStudents() {
		if (iProperties == null) return false;
		return iProperties.getCanEmailStudents();
	}
	
	public static class HistoryToken {
		private String iType = null;
		private Map<String, String> iParams = new HashMap<String, String>();
		private Map<String, String> iDefaults = new HashMap<String, String>();
		
		public HistoryToken(PageType type) {
			iType = type.name();
			
			// 1. take page type defaults --> DEFAULTS
			if (type.getParams() != null)
				for (int i = 0; 1 + i < type.getParams().length; i += 2)
					iDefaults.put(type.getParams()[i], type.getParams()[i + 1]);

			// 2. take page parameters --> DEFAULTS (on top of the page type defaults)
			for (Map.Entry<String, List<String>> params: Window.Location.getParameterMap().entrySet())
				iDefaults.put(params.getKey(), params.getValue().get(0));
			
			// 3. take cookie --> PARAMS (override defaults)
			String cookie = EventCookie.getInstance().getHash(iType);
			if (cookie != null) {
				for (String pair: cookie.split("\\&")) {
					int idx = pair.indexOf('=');
					if (idx >= 0) {
						String key = pair.substring(0, idx);
						if (Location.getParameter(key) == null)
							iParams.put(key, URL.decodeQueryString(pair.substring(idx + 1)));
					}
				}
			}			
			
			// 4. take page token (hash) --> PARAMS (override cookie)
			parse(History.getToken());
		}
		
		public void reset(String token) {
			iParams.clear();
			parse(token);
		}
		
		public void parse(String token) {
			if (token != null && !token.isEmpty())
				for (String pair: token.split("\\&")) {
					int idx = pair.indexOf('=');
					if (idx >= 0)
						iParams.put(pair.substring(0, idx), URL.decodeQueryString(pair.substring(idx + 1)));
				}
		}
		
		public void setParameter(String key, String value) {
			if (value == null) {
				iParams.remove(key);
			} else {
				String defaultValue = iDefaults.get(key);
				if (value.equals(defaultValue))
					iParams.remove(key);
				else
					iParams.put(key, value);
			}
		}
		
		public void setParameter(String key, Long value) {
			setParameter(key, value == null ? null : value.toString());
		}
		
		@Override
		public String toString() {
			return toString(null);
		}
		
		public String toString(String skip) {
			String ret = "";
			for (String key: new TreeSet<String>(iParams.keySet())) {
				if (key.equals(skip)) continue;
				if (!ret.isEmpty()) ret += "&";
				ret += key + "=" + URL.encodeQueryString(iParams.get(key));
			}
			return ret;
		}
		
		public String getParameter(String key, String defaultValue) {
			String value = getParameter(key);
			return (value == null ? defaultValue : value);
		}
		
		public String getParameter(String key) {
			String value = iParams.get(key);
			return (value == null ? iDefaults.get(key) : value);
		}
		
		public String getDefaultParameter(String key, String defaultValue) {
			String value = iDefaults.get(key);
			return (value == null ? defaultValue : value);
		}
		
		public void setDefaultParameter(String key, String value) {
			if (value != null)
				iDefaults.put(key, value);
		}
		
		public boolean hasParameter(String key) {
			return getParameter(key) != null;
		}
		
		public boolean isChanged(String key, String value) {
			String v = getParameter(key);
			return (v == null ? value != null : !v.equals(value));
		}
		
		public boolean isChanged(String key, String defaultValue, String value) {
			String v = getParameter(key);
			return (v == null ? !defaultValue.equals(value) : !v.equals(value));
		}
		
		public void mark() {
			String token = toString();
			if (!History.getToken().equals(token))
				History.newItem(token, false);
			EventCookie.getInstance().setHash(iType, toString("event"));
		}
	}
	
	public String getPageName() {
		String pageName = iHistoryToken.getParameter("title", "Events");
		ResourceType resource = getResourceType();
		if (!"true".equals(iHistoryToken.getParameter("fixedTitle")) && resource != null)
			pageName = resource.getPageName();
		return pageName;
	}

	@Override
	public boolean accept(AcademicSession session) {
		String filter = iHistoryToken.getParameter("filter");
		if ("classes".equals(filter))
			return session.has(AcademicSession.Flag.HasClasses);
		if ("exams".equals(filter))
			return session.has(AcademicSession.Flag.HasFinalExams) || session.has(AcademicSession.Flag.HasMidtermExams);
		if ("events".equals(filter))
			return session.has(AcademicSession.Flag.HasEvents);
		if ("person".equals(filter))
			return session.has(AcademicSession.Flag.HasClasses) || session.has(AcademicSession.Flag.HasFinalExams) || session.has(AcademicSession.Flag.HasMidtermExams);
		return true;
	}

	@Override
	public SessionMonth.Flag getDateFlag(EventType type, Date date) {
		if (iSessionMonths == null || iSessionMonths.isEmpty()) return null;
		if (date == null) return null;
		int m = Integer.parseInt(ServerDateTimeFormat.getFormat("MM").format(date));
		for (SessionMonth month: iSessionMonths)
			if (m == month.getMonth() + 1) {
				int d = Integer.parseInt(ServerDateTimeFormat.getFormat("dd").format(date)) - 1;
				if (month.hasFlag(d, SessionMonth.Flag.FINALS) && type != EventType.FinalExam) return SessionMonth.Flag.FINALS;
				if (month.hasFlag(d, SessionMonth.Flag.BREAK)) return SessionMonth.Flag.BREAK;
				if (month.hasFlag(d, SessionMonth.Flag.WEEKEND)) return SessionMonth.Flag.WEEKEND;
				if (month.hasFlag(d, SessionMonth.Flag.HOLIDAY)) return SessionMonth.Flag.HOLIDAY;
				if (month.hasFlag(d, SessionMonth.Flag.MIDTERMS) && type != EventType.MidtermExam) return SessionMonth.Flag.MIDTERMS;
				return null;
			}
		return null;
	}
}
