/*
 * 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.Comparator;
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.GwtHint;
import org.unitime.timetable.gwt.client.Lookup;
import org.unitime.timetable.gwt.client.ToolBox;
import org.unitime.timetable.gwt.client.events.EventAdd.EventPropertiesProvider;
import org.unitime.timetable.gwt.client.events.EventComparator.EventMeetingSortBy;
import org.unitime.timetable.gwt.client.page.UniTimeNotifications;
import org.unitime.timetable.gwt.client.sectioning.CourseDetailsWidget.CourseDetailsRpcRequest;
import org.unitime.timetable.gwt.client.sectioning.CourseDetailsWidget.CourseDetailsRpcResponse;
import org.unitime.timetable.gwt.client.widgets.NumberBox;
import org.unitime.timetable.gwt.client.widgets.P;
import org.unitime.timetable.gwt.client.widgets.SimpleForm;
import org.unitime.timetable.gwt.client.widgets.ServerDateTimeFormat;
import org.unitime.timetable.gwt.client.widgets.UniTimeDialogBox;
import org.unitime.timetable.gwt.client.widgets.UniTimeFrameDialog;
import org.unitime.timetable.gwt.client.widgets.UniTimeHeaderPanel;
import org.unitime.timetable.gwt.client.widgets.UniTimeTable;
import org.unitime.timetable.gwt.client.widgets.UniTimeTableHeader;
import org.unitime.timetable.gwt.client.widgets.TimeSelector.TimeUtils;
import org.unitime.timetable.gwt.client.widgets.UniTimeTableHeader.AriaOperation;
import org.unitime.timetable.gwt.client.widgets.UniTimeTableHeader.Operation;
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.PersonInterface;
import org.unitime.timetable.gwt.shared.EventInterface.ApprovalStatus;
import org.unitime.timetable.gwt.shared.EventInterface.ContactInterface;
import org.unitime.timetable.gwt.shared.EventInterface.EventFlag;
import org.unitime.timetable.gwt.shared.EventInterface.EventServiceProviderInterface;
import org.unitime.timetable.gwt.shared.EventInterface.EventType;
import org.unitime.timetable.gwt.shared.EventInterface.MeetingConflictInterface;
import org.unitime.timetable.gwt.shared.EventInterface.MeetingInterface;
import org.unitime.timetable.gwt.shared.EventInterface.MultiMeetingInterface;
import org.unitime.timetable.gwt.shared.EventInterface.NoteInterface;
import org.unitime.timetable.gwt.shared.EventInterface.SessionMonth;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
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.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Widget;

/**
 * @author Tomas Muller
 */
public class EventMeetingTable extends UniTimeTable<EventMeetingTable.EventMeetingRow> implements HasValue<List<EventMeetingTable.EventMeetingRow>> {
	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);
	protected static GwtRpcServiceAsync RPC = GWT.create(GwtRpcService.class);
	private static DateTimeFormat sDateFormatApproval = DateTimeFormat.getFormat(CONSTANTS.eventDateFormat());
	private static DateTimeFormat sDateFormatExpiration = ServerDateTimeFormat.getFormat(CONSTANTS.eventDateFormat());
	private static DateTimeFormat sDateFormatShort = ServerDateTimeFormat.getFormat(CONSTANTS.eventDateFormatShort());
	private static DateTimeFormat sDateFormatLong = ServerDateTimeFormat.getFormat(CONSTANTS.eventDateFormatLong());
	private static DateTimeFormat sDateFormatMeeting = ServerDateTimeFormat.getFormat(CONSTANTS.meetingDateFormat());
	private static DateTimeFormat sDateFormatAria = ServerDateTimeFormat.getFormat(CONSTANTS.dateSelectionDateFormat());
	
	public static enum ModeFlag {
		ShowEventDetails,
		ShowMeetings,
		ShowOptionalColumns,
		MustShowApproval,
		AllowApproveAll,
		HideTitle,
		CanHideDuplicitiesForMeetings,
		;
		
		public int flag() { return 1 << ordinal(); }
		public boolean in(int flags) {
			return (flags & flag()) != 0;
		}
		public int set(int flags) {
			return (in(flags) ? flags : flags + flag());
		}
		public int clear(int flags) {
			return (in(flags) ? flags - flag() : flags);
		}
	}
	
	public static enum Mode {
		ListOfEvents(ModeFlag.ShowEventDetails, ModeFlag.ShowOptionalColumns, ModeFlag.CanHideDuplicitiesForMeetings),
		ListOfMeetings(ModeFlag.ShowEventDetails, ModeFlag.ShowMeetings, ModeFlag.ShowOptionalColumns),
		MeetingsOfAnEvent(ModeFlag.ShowMeetings, ModeFlag.ShowOptionalColumns, ModeFlag.MustShowApproval, ModeFlag.AllowApproveAll, ModeFlag.HideTitle),
		ApprovalOfEvents(ModeFlag.ShowEventDetails, ModeFlag.MustShowApproval),
		ApprovalOfMeetings(ModeFlag.ShowEventDetails, ModeFlag.ShowMeetings, ModeFlag.MustShowApproval),
		ApprovalOfSingleEventMeetings(ModeFlag.ShowMeetings, ModeFlag.MustShowApproval);
		
		private int iFlags = 0;
		Mode(ModeFlag... flags) {
			for (ModeFlag flag: flags)
				iFlags = flag.set(iFlags);
		}
		
		public boolean hasFlag(ModeFlag flag) { return flag.in(iFlags); }
	}
	
	public static enum OperationType {
		Approve, Reject, Inquire, AddMeetings, Cancel, Delete, Modify
	}
	
	private Mode iMode = null;
	
	private boolean iShowMainContact = false;
	private EventMeetingSortBy iSortBy = null;
	private boolean iAsc = true;
	private boolean iSelectable = true, iEditable = false;
	private Map<OperationType, Implementation> iImplementations = new HashMap<OperationType, Implementation>();
	private MeetingFilter iMeetingFilter = null;
	private EventPropertiesProvider iPropertiesProvider = null;
	private boolean iShowMeetingContacts = false;

	public EventMeetingTable(Mode mode, boolean selectable, EventPropertiesProvider properties) {
		setStyleName("unitime-EventMeetings");
		
		iMode = mode;
		iSelectable = selectable;
		iPropertiesProvider = properties;
		
		if (getRowCount() > 0) clearTable();
		
		List<UniTimeTableHeader> header = new ArrayList<UniTimeTableHeader>();
		
		UniTimeTableHeader hTimes = new UniTimeTableHeader("&otimes;", HasHorizontalAlignment.ALIGN_CENTER);
		header.add(hTimes);
		hTimes.addOperation(new Operation() {
			@Override
			public void execute() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						ch.setValue(true);
					}
				}
			}
			@Override
			public boolean isApplicable() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (!ch.getValue()) return true;
					}
				}
				return false;
			}
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return MESSAGES.opSelectAll();
			}
		});
		hTimes.addOperation(new Operation() {
			@Override
			public void execute() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						ch.setValue(getData(row).inConflict());
					}
				}
			}
			
			@Override
			public boolean isApplicable() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox)
						if (getData(row).inConflict()) return true;
				}
				return false;
			}
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return MESSAGES.opSelectAllConflicting();
			}
		});
		hTimes.addOperation(new Operation() {
			@Override
			public void execute() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						ch.setValue(false);
					}
				}
			}
			@Override
			public boolean isApplicable() {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (ch.getValue()) return true;
					}
				}
				return false;
			}
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return MESSAGES.opClearSelection();
			}
		});
		hTimes.addOperation(new Operation() {
			@Override
			public void execute() {
				getOperation(OperationType.AddMeetings).execute(EventMeetingTable.this, OperationType.AddMeetings, null);
			}
			@Override
			public boolean isApplicable() {
				return hasOperation(OperationType.AddMeetings);
			}
			@Override
			public boolean hasSeparator() {
				return true;
			}
			@Override
			public String getName() {
				return MESSAGES.opAddMeetings();
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return (hasSelection() ? MESSAGES.opDeleteSelectedMeetings() : MESSAGES.opDeleteNewMeetings());
			}
			@Override
			public boolean isApplicable(EventMeetingRow row) {
				return isEditable() && row.isCanDelete();
			}
			@Override
			public void execute(int row, EventMeetingRow event) {
				while (row + 1 < getRowCount() && getData(row + 1).hasParent())
					removeRow(row + 1);
				if (getData(row).getMeeting().getId() == null)
					removeRow(row);
				else {
					MeetingInterface meeting = getData(row).getMeeting();
					meeting.setApprovalStatus(ApprovalStatus.Deleted);
					meeting.setCanApprove(false); meeting.setCanCancel(false); meeting.setCanInquire(false); meeting.setCanEdit(false); meeting.setCanDelete(false);
					getRowFormatter().addStyleName(row, "deleted-row");
					setWidget(row, 0, new HTML("&nbsp;"));
					HTML approval = (HTML)getWidget(row, getHeader(MESSAGES.colApproval()).getColumn());
					approval.setStyleName("deleted-meeting");
					approval.setText(MESSAGES.approvalDeleted());
					ValueChangeEvent.fire(EventMeetingTable.this, getValue());
				}
			}
			@Override
			public void execute() {
				super.execute();
				if (hasOperation(OperationType.Delete))
					getOperation(OperationType.Delete).execute(EventMeetingTable.this, OperationType.Delete, null);
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public boolean hasSeparator() {
				return true;
			}
			@Override
			public boolean allMustMatch(boolean hasSelection) {
				return false;
			}
			@Override
			public String getName() {
				return (hasSelection() ? MESSAGES.opInquireSelectedMeetings() : MESSAGES.opInquireAllMeetings());
			}
			@Override
			public boolean isApplicable(EventMeetingRow row) {
				return hasOperation(OperationType.Inquire) && row.isCanInquire();
			}
			@Override
			public void execute(int row, EventMeetingRow event) {
			}
			@Override
			public void execute() {
				getOperation(OperationType.Inquire).execute(EventMeetingTable.this, OperationType.Inquire, data());
			}
			@Override
			public boolean allowNoSelection() {
				return getMode().hasFlag(ModeFlag.AllowApproveAll);
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return (hasSelection() ? MESSAGES.opApproveSelectedMeetings() : MESSAGES.opApproveAllMeetings());
			}
			@Override
			public boolean isApplicable(EventMeetingRow row) {
				return hasOperation(OperationType.Approve) && row.isCanApprove();
			}
			@Override
			public void execute(int row, EventMeetingRow event) {
			}
			@Override
			public void execute() {
				getOperation(OperationType.Approve).execute(EventMeetingTable.this, OperationType.Approve, data());
			}
			@Override
			public boolean allowNoSelection() {
				return getMode().hasFlag(ModeFlag.AllowApproveAll);
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				if (hasOperation(OperationType.Delete))
					return (hasSelection() ? MESSAGES.opCancelSelectedMeetingsNoPopup() : MESSAGES.opCancelAllMeetingsNoPopup());
				else
					return (hasSelection() ? MESSAGES.opCancelSelectedMeetings() : MESSAGES.opCancelAllMeetings());
			}
			@Override
			public boolean isApplicable(EventMeetingRow row) {
				return (hasOperation(OperationType.Cancel) && row.isCanCancel()) || (isEditable() && row.isCanCancel());
			}
			@Override
			public void execute(int row, EventMeetingRow event) {
				if (isEditable()) {
					while (row + 1 < getRowCount() && getData(row + 1).hasParent())
						removeRow(row + 1);
					if (getData(row).getMeeting().getId() == null)
						removeRow(row);
					else {
						getData(row).getMeeting().setApprovalStatus(ApprovalStatus.Cancelled);
						getRowFormatter().addStyleName(row, "cancelled-row");
						setWidget(row, 0, new HTML("&nbsp;"));
						HTML approval = (HTML)getWidget(row, getHeader(MESSAGES.colApproval()).getColumn());
						approval.setStyleName("cancelled-meeting");
						approval.setText(MESSAGES.approvalCancelled());
						ValueChangeEvent.fire(EventMeetingTable.this, getValue());
					}					
				}
			}
			@Override
			public void execute() {
				super.execute();
				if (hasOperation(OperationType.Cancel))
					getOperation(OperationType.Cancel).execute(EventMeetingTable.this, OperationType.Cancel, data());
			}
			@Override
			public boolean allowNoSelection() {
				return getMode().hasFlag(ModeFlag.AllowApproveAll);
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public boolean hasSeparator() {
				return false;
			}
			@Override
			public String getName() {
				return (hasSelection() ? MESSAGES.opRejectSelectedMeetings() : MESSAGES.opRejectAllMeetings());
			}
			@Override
			public boolean isApplicable(EventMeetingRow row) {
				return hasOperation(OperationType.Reject) && row.isCanApprove();
			}
			@Override
			public void execute(int row, EventMeetingRow event) {
			}
			@Override
			public void execute() {
				getOperation(OperationType.Reject).execute(EventMeetingTable.this, OperationType.Reject, data());
			}
			@Override
			public boolean allowNoSelection() {
				return getMode().hasFlag(ModeFlag.AllowApproveAll);
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
            public void execute() {
				getOperation(OperationType.Modify).execute(EventMeetingTable.this, OperationType.Modify, data());
			}
            @Override
            public String getName() {
                    return MESSAGES.opModifyMeetings();
            }
    		@Override
    		public boolean isApplicable() {
    			Integer start = null, end = null;
    			boolean hasSelection = hasSelection();
    			if (hasSelection) {
    				for (int row = 1; row < getRowCount(); row++) {
    					Widget w =  getWidget(row, 0);
    					if (w != null && w instanceof CheckBox) {
    						CheckBox ch = (CheckBox)w;
    						if (ch.getValue()) {
    							EventMeetingRow e = getData(row);
    							if (!isApplicable(e)) return false;
								if (start == null) { start = e.getMeeting().getStartSlot(); end = e.getMeeting().getEndSlot(); }
								else if (start != e.getMeeting().getStartSlot() || end != e.getMeeting().getEndSlot()) return false;
    						}
    					}
    				}
    				return true;
    			} else if (allowNoSelection()) {
    				boolean canSelect = false;
    				for (int row = 1; row < getRowCount(); row++) {
    					Widget w =  getWidget(row, 0);
    					if (w != null && w instanceof CheckBox) {
    						EventMeetingRow e = getData(row);
    						if (!isApplicable(e)) return false;
							if (start == null) { start = e.getMeeting().getStartSlot(); end = e.getMeeting().getEndSlot(); }
							else if (start != e.getMeeting().getStartSlot() || end != e.getMeeting().getEndSlot()) return false;
							canSelect = true;
    					}
    				}
    				return canSelect;
    			} else {
    				return false;
    			}
    		}
    		@Override
    		public boolean allMustMatch(boolean hasSelection) {
    			return true;
    		}
    		@Override
            public boolean isApplicable(EventMeetingRow data) {
                    return isEditable() && hasOperation(OperationType.Modify) && data.getMeeting() != null && (data.getMeeting().getId() == null || data.getMeeting().isCanDelete() || data.getMeeting().isCanCancel());
            }
            @Override
            public void execute(int row, EventMeetingRow data) {}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
            public void execute() {
				Integer so = null, eo = null;
                boolean soSame = true, eoSame = true;
                for (EventMeetingRow r: data()) {
                	MeetingInterface m = r.getMeeting();
                	if (so == null) so = m.getStartOffset();
                    else if (m.getStartOffset() != so) soSame = false;
                    if (eo == null) eo = -m.getEndOffset();
                    else if (-m.getEndOffset() != eo) eoSame = false;
                }
                final UniTimeDialogBox dialog = new UniTimeDialogBox(true, false);
                SimpleForm simple = new SimpleForm();
                simple.removeStyleName("unitime-NotPrintableBottomLine");
                final NumberBox setupTime = new NumberBox();
                if (soSame && so != null) setupTime.setValue(so); 
                simple.addRow(MESSAGES.propSetupTime(), setupTime);
                final NumberBox teardownTime = new NumberBox();
                if (eoSame && eo != null) teardownTime.setValue(eo);
                simple.addRow(MESSAGES.propTeardownTime(), teardownTime);
                UniTimeHeaderPanel footer = new UniTimeHeaderPanel();
                footer.addButton("ok", MESSAGES.buttonOk(), 75, new ClickHandler() {
                	@Override
                	public void onClick(ClickEvent event) {
                		int colSetup = getHeader(MESSAGES.colSetupTimeShort()).getColumn();
                        int colTear = getHeader(MESSAGES.colTeardownTimeShort()).getColumn();
                        int colPubl = getHeader(MESSAGES.colPublishedTime()).getColumn();
                        for (Integer row: rows()) {
                        	MeetingInterface meeting = getData(row).getMeeting();
                        	if (setupTime.toInteger() != null)
                        		meeting.setStartOffset(setupTime.toInteger());
                            if (teardownTime.toInteger() != null)
                                meeting.setEndOffset(-teardownTime.toInteger());
                            ((NumberCell)getWidget(row, colSetup)).setText(String.valueOf(meeting.getStartOffset()));
                            ((NumberCell)getWidget(row, colTear)).setText(String.valueOf(-meeting.getEndOffset()));
                            ((Label)getWidget(row, colPubl)).setText(meeting.getMeetingTime(CONSTANTS));
                        }
                        dialog.hide();
                	}
                });
                footer.addButton("cancel", MESSAGES.buttonCancel(), 75, new ClickHandler() {
                	@Override
                	public void onClick(ClickEvent event) {
                		dialog.hide();
                	}
                });
                simple.addBottomRow(footer);
                dialog.setWidget(simple);
                dialog.setText(MESSAGES.dlgChangeOffsets());
                dialog.setEscapeToHide(true);
                dialog.center();
			}
            @Override
            public String getName() {
                    return MESSAGES.opChangeOffsets();
            }
            @Override
            public boolean isApplicable(EventMeetingRow data) {
                    return isEditable() && data.getMeeting() != null && (data.getMeeting().getId() == null || data.getMeeting().isCanEdit());
            }
            @Override
            public void execute(int row, EventMeetingRow data) {}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			Lookup iLookup = null;

			@Override
			public String getName() {
				return MESSAGES.opAddMeetingContact();
			}
			
			@Override
			public boolean isApplicable(EventMeetingRow data) {
				return iPropertiesProvider.getProperties().isCanEditMeetingContacts() && isEditable() && data.getMeeting() != null && (data.getMeeting().getId() == null || data.getMeeting().isCanEdit());
			}
			
			@Override
			public void execute(int row, EventMeetingRow event) {}
			
			@Override
            public void execute() {
				if (iLookup == null) {
					iLookup = new Lookup();
					iLookup.addValueChangeHandler(new ValueChangeHandler<PersonInterface>() {
						@Override
						public void onValueChange(ValueChangeEvent<PersonInterface> event) {
							int colMeetingContacts = getHeader(MESSAGES.colMeetingContacts()).getColumn();
							if (event.getValue() != null) {
								ContactInterface contact =  new ContactInterface(event.getValue());
								for (Integer row: rows()) {
		                        	MeetingInterface meeting = getData(row).getMeeting();
		                        	meeting.addMeetingContact(contact);
		                        	((HTML)getWidget(row, colMeetingContacts)).setHTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES));
		                        }
								setColumnVisible(colMeetingContacts, true);
							}
						}
					});
				}
				iLookup.setOptions("mustHaveExternalId" + (iPropertiesProvider.getSessionId() == null ? "" : ",session=" + iPropertiesProvider.getSessionId()));
				iLookup.center();
			}
		});
		hTimes.addOperation(new EventMeetingOperation() {
			@Override
			public String getName() {
				return MESSAGES.opRemoveMeetingContact();
			}
			
			@Override
			public boolean isApplicable(EventMeetingRow data) {
				return iPropertiesProvider.getProperties().isCanEditMeetingContacts() &&
						isColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn()) &&
						isEditable() && data.getMeeting() != null && (data.getMeeting().getId() == null || data.getMeeting().isCanEdit());
			}
			
			@Override
			public void execute(int row, EventMeetingRow event) {}
			
			@Override
            public void execute() {
				final TreeSet<ContactInterface> contacts = new TreeSet<ContactInterface>();
				for (Integer row: rows()) {
                	MeetingInterface meeting = getData(row).getMeeting();
                	if (meeting.hasMeetingContacts()) contacts.addAll(meeting.getMeetingContacts());
				}
				if (contacts.isEmpty()) {
					UniTimeNotifications.warn(MESSAGES.warnSelectedMeetingsHaveNoMeetingContacts());
				} else if (contacts.size() == 1) {
					ContactInterface contact = contacts.first();
					int colMeetingContacts = getHeader(MESSAGES.colMeetingContacts()).getColumn();
					for (Integer row: rows()) {
                    	MeetingInterface meeting = getData(row).getMeeting();
                    	if (meeting.hasMeetingContacts()) {
                    		meeting.getMeetingContacts().remove(contact);
                        	((HTML)getWidget(row, colMeetingContacts)).setHTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES));
                    	}
                    }
				} else {
					final UniTimeDialogBox dialog = new UniTimeDialogBox(true, false);
					final ListBox list = new ListBox();
					list.setMultipleSelect(true);
					for (ContactInterface contact: contacts) {
						list.addItem(contact.getName(MESSAGES));
					}
					ToolBox.setMinWidth(list.getElement().getStyle(), "200px");
	                SimpleForm simple = new SimpleForm();
	                simple.removeStyleName("unitime-NotPrintableBottomLine");
	                simple.addRow(MESSAGES.propMeetingContacts(), list);
	                UniTimeHeaderPanel footer = new UniTimeHeaderPanel();
	                footer.addButton("selected", MESSAGES.buttonRemoveSelected(), 75, new ClickHandler() {
	                	@Override
	                	public void onClick(ClickEvent event) {
	                		int colMeetingContacts = getHeader(MESSAGES.colMeetingContacts()).getColumn();
	                		for (Integer row: rows()) {
	                        	MeetingInterface meeting = getData(row).getMeeting();
	                        	if (!meeting.hasMeetingContacts()) continue;
	                        	int i = 0;
	                        	for (ContactInterface contact: contacts) {
	                        		if (list.isItemSelected(i++)) {
	                        			meeting.getMeetingContacts().remove(contact);
	                        		}
	                        	}
	                        	((HTML)getWidget(row, colMeetingContacts)).setHTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES));
	                		}
	                		dialog.hide();
	                	}
	                });
	                footer.addButton("all", MESSAGES.buttonRemoveAll(), 75, new ClickHandler() {
	                	@Override
	                	public void onClick(ClickEvent event) {
	                		int colMeetingContacts = getHeader(MESSAGES.colMeetingContacts()).getColumn();
	                		for (Integer row: rows()) {
	                        	MeetingInterface meeting = getData(row).getMeeting();
	                        	if (meeting.hasMeetingContacts()) meeting.getMeetingContacts().clear();
	                            	((HTML)getWidget(row, colMeetingContacts)).setHTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES));
	                		}
	                		dialog.hide();
	                	}
	                });
	                footer.addButton("cancel", MESSAGES.buttonCancel(), 75, new ClickHandler() {
	                	@Override
	                	public void onClick(ClickEvent event) {
	                		dialog.hide();
	                	}
	                });
	                simple.addBottomRow(footer);
	                dialog.setWidget(simple);
	                dialog.setText(MESSAGES.dlgRemoveMeetingContacts());
	                dialog.setEscapeToHide(true);
	                dialog.setEnterToSubmit(new Command() {
						@Override
						public void execute() {
							int colMeetingContacts = getHeader(MESSAGES.colMeetingContacts()).getColumn();
	                		for (Integer row: rows()) {
	                        	MeetingInterface meeting = getData(row).getMeeting();
	                        	if (!meeting.hasMeetingContacts()) continue;
	                        	int i = 0;
	                        	for (ContactInterface contact: contacts) {
	                        		if (list.isItemSelected(i++)) {
	                        			meeting.getMeetingContacts().remove(contact);
	                        		}
	                        	}
	                        	((HTML)getWidget(row, colMeetingContacts)).setHTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES));
	                		}
	                		dialog.hide();
						}
					});
	                dialog.center();
	                list.setFocus(true);
				}
			}
		});
		
		UniTimeTableHeader hName = new UniTimeTableHeader(MESSAGES.colName());
		header.add(hName);

		UniTimeTableHeader hSection = new UniTimeTableHeader(MESSAGES.colSection(), HasHorizontalAlignment.ALIGN_RIGHT);
		header.add(hSection);

		UniTimeTableHeader hType = new UniTimeTableHeader(MESSAGES.colType());
		header.add(hType);
		
		UniTimeTableHeader hTitle = new UniTimeTableHeader(MESSAGES.colTitle());
		header.add(hTitle);

		UniTimeTableHeader hNote = new UniTimeTableHeader(MESSAGES.colNote());
		header.add(hNote);

		UniTimeTableHeader hDate = new UniTimeTableHeader(MESSAGES.colDate());
		header.add(hDate);
		

		UniTimeTableHeader hTimePub = new UniTimeTableHeader(MESSAGES.colPublishedTime()); 
		header.add(hTimePub);
		UniTimeTableHeader hTimeAll = new UniTimeTableHeader(MESSAGES.colAllocatedTime()); 
		header.add(hTimeAll);
		UniTimeTableHeader hTimeSetup = new UniTimeTableHeader(MESSAGES.colSetupTimeShort(), HasHorizontalAlignment.ALIGN_RIGHT); 
		header.add(hTimeSetup);
		UniTimeTableHeader hTimeTeardown = new UniTimeTableHeader(MESSAGES.colTeardownTimeShort(), HasHorizontalAlignment.ALIGN_RIGHT); 
		header.add(hTimeTeardown);

		UniTimeTableHeader hLocation = new UniTimeTableHeader(MESSAGES.colLocation());
		header.add(hLocation);
		
		UniTimeTableHeader hCapacity = new UniTimeTableHeader(MESSAGES.colCapacity(), HasHorizontalAlignment.ALIGN_RIGHT);
		header.add(hCapacity);
		
		UniTimeTableHeader hMeetingContacts = new UniTimeTableHeader(MESSAGES.colMeetingContacts());
		header.add(hMeetingContacts);

		UniTimeTableHeader hEnrollment = new UniTimeTableHeader(MESSAGES.colEnrollment(), HasHorizontalAlignment.ALIGN_RIGHT);
		header.add(hEnrollment);

		UniTimeTableHeader hLimit = new UniTimeTableHeader(MESSAGES.colLimit(), HasHorizontalAlignment.ALIGN_RIGHT);
		header.add(hLimit);

		UniTimeTableHeader hSponsor = new UniTimeTableHeader(MESSAGES.colSponsorOrInstructor());
		header.add(hSponsor);
		
		UniTimeTableHeader hRequestedServices = new UniTimeTableHeader(MESSAGES.colRequestedServices());
		header.add(hRequestedServices);

		UniTimeTableHeader hContact = new UniTimeTableHeader(MESSAGES.colMainContact());
		header.add(hContact);

		UniTimeTableHeader hApproval = new UniTimeTableHeader(MESSAGES.colApproval());
		header.add(hApproval);
		
		UniTimeTableHeader hLastChange = new UniTimeTableHeader(MESSAGES.colLastChange());
		header.add(hLastChange);

		addRow(null, header);
		
		final Operation titleOp = addHideOperation(hTitle, EventFlag.SHOW_TITLE, new Check() {
			@Override
			public boolean isChecked() {
				return true;
			}
		});
		final Operation noteOp = addHideOperation(hNote, EventFlag.SHOW_NOTE, new Check() {
			@Override
			public boolean isChecked() {
				return !titleOp.isApplicable();
			}
		});
		addHideOperation(hTimePub, EventFlag.SHOW_PUBLISHED_TIME, new Check() {
			@Override
			public boolean isChecked() {
				return !titleOp.isApplicable() && !noteOp.isApplicable();
			}
		});
		addHideOperation(hTimeAll, EventFlag.SHOW_ALLOCATED_TIME);
		addHideOperation(hTimeSetup, EventFlag.SHOW_SETUP_TIME);
		addHideOperation(hTimeTeardown, EventFlag.SHOW_TEARDOWN_TIME);
		addHideOperation(hCapacity, EventFlag.SHOW_CAPACITY);
		addHideOperation(hMeetingContacts, EventFlag.SHOW_MEETING_CONTACTS);
		addHideOperation(hEnrollment, EventFlag.SHOW_ENROLLMENT);
		addHideOperation(hLimit, EventFlag.SHOW_LIMIT);
		addHideOperation(hSponsor, EventFlag.SHOW_SPONSOR);
		addHideOperation(hRequestedServices, EventFlag.SHOW_REQUESTED_SERVICES);
		addHideOperation(hContact, EventFlag.SHOW_MAIN_CONTACT);
		addHideOperation(hApproval, EventFlag.SHOW_APPROVAL);
		addHideOperation(hLastChange, EventFlag.SHOW_LAST_CHANGE);
		
		Operation hideDuplicitiesForMeetings = new AriaOperation() {
			@Override
			public boolean isApplicable() {
				return iMode.hasFlag(ModeFlag.CanHideDuplicitiesForMeetings);
			}

			@Override
			public boolean hasSeparator() {
				return true;
			}

			@Override
			public void execute() {
				EventCookie.getInstance().setHideDuplicitiesForMeetings(!EventCookie.getInstance().isHideDuplicitiesForMeetings());
				int colDate = getHeader(MESSAGES.colDate()).getColumn();
				int colApproval = getHeader(MESSAGES.colApproval()).getColumn();
				for (int row = 1; row < getRowCount(); row++) {
					EventMeetingRow data = getData(row);
					if (data == null) continue;
					EventInterface event = data.getEvent();
					String[] mtgs = new String[] {"", "", "", "", "", "", "", ""};
					String prevApproval = null;
					String[] prev = null;
					String prevSpan = null;
					String approval = "";
					boolean globalUnavailability = event != null && event.getId() != null && event.getId() < 0 && event.getType() == EventType.Unavailabile;
					for (MultiMeetingInterface m: EventInterface.getMultiMeetings(data.getMeetings(getMeetingFilter()), true, globalUnavailability ? null : iPropertiesProvider, event == null ? null : event.getType(), iShowMeetingContacts)) {
						String[] mtg = new String[] {
								m.isArrangeHours() ? event.hasMessage() ? event.getMessage() : CONSTANTS.arrangeHours() : (m.getDays(iPropertiesProvider.getFirstDayOfWeek(), CONSTANTS) + " " + (m.getNrMeetings() == 1 ? sDateFormatLong.format(m.getFirstMeetingDate()) : sDateFormatShort.format(m.getFirstMeetingDate()) + " - " + sDateFormatLong.format(m.getLastMeetingDate()))),
								m.isArrangeHours() && event.hasMessage() ? CONSTANTS.arrangeHours() : m.getMeetings().first().getMeetingTime(CONSTANTS),
								m.getMeetings().first().getAllocatedTime(CONSTANTS),
								String.valueOf(m.getMeetings().first().getStartOffset()),
								String.valueOf(- m.getMeetings().first().getEndOffset()),
								m.getLocationNameWithHint(MESSAGES),
								(m.getMeetings().first().getLocation() == null ? "" : m.getMeetings().first().getLocation().hasSize() ? m.getMeetings().first().getLocation().getSize().toString() : MESSAGES.notApplicable()),
								iShowMeetingContacts ? m.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES) : ""
								};
						if (!m.isArrangeHours() && !m.isPast()) {
							SessionMonth.Flag dateFlag = (globalUnavailability || iPropertiesProvider == null ? null : iPropertiesProvider.getDateFlag(event == null ? null : event.getType(), m.getFirstMeetingDate()));
							if (dateFlag != null) {
								switch (dateFlag) {
								case FINALS:
									mtg[0] = "<span class='finals' title=\"" + MESSAGES.hintFinals() + "\">" + mtg[0] + "</span>";
									break;
								case MIDTERMS:
									mtg[0] = "<span class='midterms' title=\"" + MESSAGES.hintMidterms() + "\">" + mtg[0] + "</span>";
									break;
								case BREAK:
									mtg[0] = "<span class='break' title=\"" + MESSAGES.hintBreak() + "\">" + mtg[0] + "</span>";
									break;
								case HOLIDAY:
									mtg[0] = "<span class='holiday' title=\"" + MESSAGES.hintHoliday() + "\">" + mtg[0] + "</span>";
									break;
								case WEEKEND:
									mtg[0] = "<span class='weekend' title=\"" + MESSAGES.hintWeekend() + "\">" + mtg[0] + "</span>";
									break;
								}
							}
						}
						if (!m.isArrangeHours() && iPropertiesProvider != null && iPropertiesProvider.isTooEarly(m.getMeetings().first().getStartSlot(), m.getMeetings().first().getEndSlot())) {
							for (int i = 1; i <= 2; i++)
								mtg[i] = "<span class='early' title=\"" + MESSAGES.hintTooEarly() + "\">" + mtg[i] + "</span>";
						}
						String span = "";
						if (m.getApprovalStatus() == ApprovalStatus.Cancelled)
							span = "cancelled-meeting";
						else if (m.getApprovalStatus() == ApprovalStatus.Rejected)
							span = "rejected-meeting";
						else if (m.isPast())
							span = "past-meeting";
						else if (m.hasStyle())
							span = m.getStyle();
						for (int i = 0; i < mtgs.length; i++) {
							mtgs[i] += (mtgs[i].isEmpty() ? "" : "<br>") + (prev != null && span.equals(prevSpan) && prev[i == 6 ? i - 1 : i].equals(mtg[i == 6 ? i - 1 : i]) && (i != 7 || !mtg[i].isEmpty()) ? MESSAGES.repeatingSymbol() : (!span.isEmpty() ? "<span class='" + span + "'>" : "") + mtg[i] + (!span.isEmpty() ? "</span>" : ""));							
						}
						String thisApproval = (
								m.getApprovalStatus() == ApprovalStatus.Approved ? sDateFormatApproval.format(m.getApprovalDate()) :
								m.getApprovalStatus() == ApprovalStatus.Cancelled ? MESSAGES.approvalCancelled() :
								m.getApprovalStatus() == ApprovalStatus.Rejected ? MESSAGES.approvalRejected() :
								"");
									
						approval += (approval.isEmpty() ? "" : "<br>") + (prev != null && span.equals(prevSpan) && prevApproval.equals(thisApproval) ? MESSAGES.repeatingSymbol() : 
								(m.getApprovalStatus() == ApprovalStatus.Approved ?
								m.isPast() ? "<span class='past-meeting'>" + sDateFormatApproval.format(m.getApprovalDate()) + "</span>" : sDateFormatApproval.format(m.getApprovalDate()) :
								m.getApprovalStatus() == ApprovalStatus.Cancelled ? "<span class='cancelled-meeting'>" + MESSAGES.approvalCancelled() + "</span>":
								m.getApprovalStatus() == ApprovalStatus.Rejected ? "<span class='rejected-meeting'>" + MESSAGES.approvalRejected() + "</span>":
								event != null && event.getType() == EventType.Unavailabile ? "" : 
								m.getFirstMeetingDate() == null ? "" : m.isPast() ? "<span class='not-approved-past'>" + MESSAGES.approvalNotApprovedPast() + "</span>" :
								event != null && event.getExpirationDate() != null ? "<span class='not-approved'>" + MESSAGES.approvalExpire(sDateFormatExpiration.format(event.getExpirationDate())) + "</span>" : 
								"<span class='not-approved'>" + MESSAGES.approvalNotApproved() + "</span>"));
						if (EventCookie.getInstance().isHideDuplicitiesForMeetings()) {
							prev = mtg; prevSpan = span; prevApproval = thisApproval;
						}
					}
					for (int i = 0; i < mtgs.length; i++) {
						if (i == 3 || i == 4 || i == 6)
							setWidget(row, colDate + i, new NumberCell(mtgs[i]));
						else
							setWidget(row, colDate + i, new HTML(mtgs[i], false));
					}
					setWidget(row, colApproval, new HTML(approval == null ? "" : approval, false));
				}
			}

			@Override
			public String getName() {
				return EventCookie.getInstance().isHideDuplicitiesForMeetings()
						? MESSAGES.opUncheck(MESSAGES.opHideRepeatingInformation())
						: MESSAGES.opCheck(MESSAGES.opHideRepeatingInformation());
			}
			@Override
			public String getAriaLabel() {
				return EventCookie.getInstance().isHideDuplicitiesForMeetings()
						? ARIA.opUncheck(MESSAGES.opHideRepeatingInformation())
						: ARIA.opCheck(MESSAGES.opHideRepeatingInformation());
			}
		};
		hDate.addOperation(hideDuplicitiesForMeetings);
		hTimePub.addOperation(hideDuplicitiesForMeetings);
		hTimeAll.addOperation(hideDuplicitiesForMeetings);
		hTimeSetup.addOperation(hideDuplicitiesForMeetings);
		hTimeTeardown.addOperation(hideDuplicitiesForMeetings);
		hLocation.addOperation(hideDuplicitiesForMeetings);
		hCapacity.addOperation(hideDuplicitiesForMeetings);
		hApproval.addOperation(hideDuplicitiesForMeetings);
		hMeetingContacts.addOperation(hideDuplicitiesForMeetings);

		addSortByOperation(hName, EventMeetingSortBy.NAME);
		addSortByOperation(hSection, EventMeetingSortBy.SECTION);
		addSortByOperation(hType, EventMeetingSortBy.TYPE);
		addSortByOperation(hTitle, EventMeetingSortBy.TITLE);
		addSortByOperation(hNote, EventMeetingSortBy.NOTE);
		addSortByOperation(hDate, EventMeetingSortBy.DATE);
		addSortByOperation(hTimePub, EventMeetingSortBy.PUBLISHED_TIME);
		addSortByOperation(hTimeAll, EventMeetingSortBy.ALLOCATED_TIME);
		addSortByOperation(hTimeSetup, EventMeetingSortBy.SETUP_TIME);
		addSortByOperation(hTimeTeardown, EventMeetingSortBy.TEARDOWN_TIME);
		addSortByOperation(hLocation, EventMeetingSortBy.LOCATION);
		addSortByOperation(hCapacity, EventMeetingSortBy.CAPACITY);
		addSortByOperation(hMeetingContacts, EventMeetingSortBy.MEETING_CONTACTS);
		addSortByOperation(hEnrollment, EventMeetingSortBy.ENROLLMENT);
		addSortByOperation(hLimit, EventMeetingSortBy.LIMIT);
		addSortByOperation(hSponsor, EventMeetingSortBy.SPONSOR);
		addSortByOperation(hRequestedServices, EventMeetingSortBy.SERVICES);
		addSortByOperation(hContact, EventMeetingSortBy.MAIN_CONTACT);
		addSortByOperation(hApproval, EventMeetingSortBy.APPROVAL);
		addSortByOperation(hLastChange, EventMeetingSortBy.LAST_CHANGE);
		
		hTimes.addOperation(new AriaOperation() {
			@Override
			public void execute() {
				EventCookie.getInstance().setAutomaticallyApproveNewMeetings(!EventCookie.getInstance().isAutomaticallyApproveNewMeetings());
				for (int row = 1; row < getRowCount(); row++) {
					EventMeetingRow data = getData(row);
					if (data.hasMeeting() && data.getMeeting().getId() == null && (data.getMeeting().isCanApprove() || data.getMeeting().isAutomaticallyApproved()) && data.hasEvent() && (data.getEvent().getType() == EventType.Special || data.getEvent().getType() == EventType.Course)) {
						HTML approval = (HTML)getWidget(row, getHeader(MESSAGES.colApproval()).getColumn());
						if (EventCookie.getInstance().isAutomaticallyApproveNewMeetings() || data.getMeeting().isAutomaticallyApproved()) {
							approval.setStyleName("new-approved-meeting");
							approval.setText(MESSAGES.approvelNewApprovedMeeting());
						} else {
							approval.setStyleName("new-meeting");
							approval.setText(MESSAGES.approvalNewMeeting());
						}
					}
				}
				ValueChangeEvent.fire(EventMeetingTable.this, getValue());
			}
			@Override
			public boolean isApplicable() {
				for (int row = 1; row < getRowCount(); row++) {
					EventMeetingRow data = getData(row);
					if (data.hasMeeting() && data.getMeeting().getId() == null && data.getMeeting().isCanApprove() && data.hasEvent() && (data.getEvent().getType() == EventType.Special || data.getEvent().getType() == EventType.Course))
						return true;
				}
				return false;
			}
			@Override
			public boolean hasSeparator() {
				return true;
			}
			@Override
			public String getName() {
				return EventCookie.getInstance().isAutomaticallyApproveNewMeetings()
						? MESSAGES.opUncheck(MESSAGES.opAutomaticApproval())
						: MESSAGES.opCheck(MESSAGES.opAutomaticApproval());
			}
			@Override
			public String getAriaLabel() {
				return EventCookie.getInstance().isAutomaticallyApproveNewMeetings()
						? ARIA.opUncheck(MESSAGES.opAutomaticApproval())
						: ARIA.opCheck(MESSAGES.opAutomaticApproval());
			}
			
		});
		
		for (int i = 0; i < getCellCount(0); i++)
			getCellFormatter().setStyleName(0, i, "unitime-ClickableTableHeaderNoBorderLine");

		resetColumnVisibility();
	}
	
	public Mode getMode() { return iMode; }
	public void setMode(Mode mode) { iMode = mode; }
	
	public void setSelectable(boolean selectable) { iSelectable = selectable; }
	public boolean isSelectable() { return iSelectable; }
	
	public void setEditable(boolean editable) { iEditable = editable; }
	public boolean isEditable() { return iEditable; }
	
	public MeetingFilter getMeetingFilter() { return iMeetingFilter; }
	
	public void setMeetingFilter(MeetingFilter filter) { iMeetingFilter = filter; }
	public void setShowMainContact(boolean show) { iShowMainContact = show; }
	public boolean isShowMainContact() { return iShowMainContact; }
	
	public void setShowMeetingContacts(boolean show) { iShowMeetingContacts = show; }
	public boolean isShowMeetingContacts() { return iShowMeetingContacts; }
	
	public void setOperation(OperationType operation, Implementation command) {
		if (command == null)
			iImplementations.remove(operation);
		else
			iImplementations.put(operation, command);
	}
	public Implementation getOperation(OperationType operation) { return iImplementations.get(operation); }
	public boolean hasOperation(OperationType operation) { return getOperation(operation) != null; }
	
	protected boolean isSelectable(EventMeetingRow data) {
		return (hasOperation(OperationType.Approve) && data.isCanApprove()) ||
				(hasOperation(OperationType.Cancel) && data.isCanCancel() && (!getMode().hasFlag(ModeFlag.ShowEventDetails) || isShowMainContact())) ||
				(hasOperation(OperationType.Inquire) && data.isCanInquire()) ||
				(isEditable() && (data.isEditable() || data.isCanCancel() || data.isCanDelete()));
	}
	
	public void add(EventMeetingRow data) {
		if (!getMode().hasFlag(ModeFlag.ShowMeetings) && data.getMeetings(getMeetingFilter()).isEmpty()) return;
		
		List<Widget> row = new ArrayList<Widget>();
		
		EventInterface event = data.getEvent();
		MeetingInterface meeting = data.getMeeting();
		MeetingConflictInterface conflict = (meeting instanceof MeetingConflictInterface ? (MeetingConflictInterface) meeting : null );
		if (conflict != null && conflict.getType() == EventType.Message && getMode().hasFlag(ModeFlag.ShowEventDetails)) return;
		
		if (data.hasParent()) {
			if (conflict != null && conflict.getType() == EventType.Message)
				row.add(new CenterredCell(MESSAGES.signMessage()));
			else
				row.add(new CenterredCell(MESSAGES.signConflict()));
		} else if (!isSelectable()) {
			row.add(new HTML(MESSAGES.signSelected()));
		} else if (isSelectable(data)) {
			CheckBoxCell check = new CheckBoxCell();
			check.setAriaLabel(data.toAriaString(getMode().hasFlag(ModeFlag.ShowEventDetails)));
			row.add(check);
			if (!isColumnVisible(0)) setColumnVisible(0, true);
		} else {
			row.add(new HTML("&nbsp;"));
		}
		
		if (event != null && event.getType() != null) {
			if (event.hasCourseNames()) {
				List<P> name = new ArrayList<P>();
				List<String> section = new ArrayList<String>();
				List<String> title = new ArrayList<String>();
				if (event.getType() == EventType.Course) { name.add(new CourseName(null, event.getName())); section.add("&nbsp;"); }
				int idx = 0;
				for (String cn: event.getCourseNames()) {
					Long courseId = event.getCourseId(idx++);
					if (name.isEmpty()) {
						name.add(new CourseName(courseId, cn));
					} else if (event.getInstruction() != null || event.getType() == EventType.Course) {
						name.add(new CourseName(courseId, cn, "no-control"));
					} else {
						name.add(new CourseName(courseId, cn));
					}
				}
				if (event.hasExternalIds())
					for (String ex: event.getExternalIds()) {
						if (section.isEmpty()) {
							section.add(ex);
						} else if (event.getInstruction() != null || event.getType() == EventType.Course) {
							section.add("<span class='no-control'>" + ex + "</span>");
						} else {
							section.add(ex);
						}
					}
				else if (event.hasSectionNumber()) {
					section.clear(); section.add(event.getSectionNumber());
				}
				if (event.hasCourseTitles()) {
					String last = null;
					for (String ct: event.getCourseTitles()) {
						if (last != null && !last.isEmpty() && last.equals(ct))
							ct = "";
						else
							last = ct;
						if (title.isEmpty()) {
							title.add(ct);
						} else if (event.getInstruction() != null || event.getType() == EventType.Course) {
							title.add("<span class='no-control'>" + ct + "</span>");
						} else {
							title.add(ct);
						}
					}
				}
				row.add(new MultiLinePCell(name));
				row.add(new MultiLineNumberCell(section));
				row.add(new Label(event.getInstruction() == null ? event.getType().getAbbreviation(CONSTANTS) : event.getInstruction(), false));
				row.add(new MultiLineStringCell(title));
				if (event.hasEventNote() && getMode().hasFlag(ModeFlag.ShowEventDetails)) {
					MultiLineStringCell note = new MultiLineStringCell(event.getEventNote("\n").split("\\n"));
					note.setTitle(event.getEventNote("\n"));
					note.addStyleName("note");
					note.getElement().getStyle().clearWhiteSpace();
					row.add(note);
				} else {
					row.add(new HTML("&nbsp;"));	
				}
				if (!section.isEmpty() && !isColumnVisible(getHeader(MESSAGES.colSection()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_TITLE) && getMode().hasFlag(ModeFlag.ShowOptionalColumns) && !getMode().hasFlag(ModeFlag.HideTitle))
					setColumnVisible(getHeader(MESSAGES.colSection()).getColumn(), true);
				if (!title.isEmpty() && !isColumnVisible(getHeader(MESSAGES.colTitle()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_TITLE) && getMode().hasFlag(ModeFlag.ShowOptionalColumns) && !getMode().hasFlag(ModeFlag.HideTitle)) 
					setColumnVisible(getHeader(MESSAGES.colTitle()).getColumn(), true);
				if (event.hasEventNote() && !isColumnVisible(getHeader(MESSAGES.colNote()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_NOTE) && getMode().hasFlag(ModeFlag.ShowOptionalColumns) && !getMode().hasFlag(ModeFlag.HideTitle)) 
					setColumnVisible(getHeader(MESSAGES.colNote()).getColumn(), true);
			} else {
				row.add(new HTML(event.getName()));
				row.add(new HTML("&nbsp;"));
				row.add(new Label(event.getType().getAbbreviation(CONSTANTS), false));
				row.add(new HTML("&nbsp;"));
				if (event.hasEventNote() && getMode().hasFlag(ModeFlag.ShowEventDetails)) {
					MultiLineStringCell note = new MultiLineStringCell(event.getEventNote("\n").split("\\n"));
					note.setTitle(event.getEventNote("\n"));
					note.addStyleName("note");
					note.getElement().getStyle().clearWhiteSpace();
					row.add(note);
				} else {
					row.add(new HTML("&nbsp;"));	
				}
				if (event.hasEventNote() && !isColumnVisible(getHeader(MESSAGES.colNote()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_NOTE) && getMode().hasFlag(ModeFlag.ShowOptionalColumns) && !getMode().hasFlag(ModeFlag.HideTitle)) 
					setColumnVisible(getHeader(MESSAGES.colNote()).getColumn(), true);
			}
		} else if (conflict != null) {
			row.add(new HTML(conflict.getName()));
			row.add(new HTML("&nbsp;"));
			row.add(new HTML(conflict.getType().getAbbreviation(CONSTANTS), false));
			row.add(new HTML("&nbsp;"));
			row.add(new HTML("&nbsp;"));
		} else {
			row.add(new HTML());
			row.add(new HTML());
			row.add(new HTML());
			row.add(new HTML());
			row.add(new HTML());
		}

		String approval = "";
		boolean allCancelledOrRejected = false;
		if (meeting != null) {
			if (conflict != null && (conflict.getType() == EventType.Message || conflict.getType() == EventType.Unavailabile) && conflict.isAllDay() && !getMode().hasFlag(ModeFlag.ShowEventDetails)) {
				row.add(new HTMLWithColSpan(conflict.getName(), true, 5));
				row.get(row.size() - 1).addStyleName("indent");
			} else {
				if (conflict != null && !getMode().hasFlag(ModeFlag.ShowEventDetails)) {
					row.add(new HTML(conflict.getType() == EventType.Unavailabile || conflict.getType() == EventType.Message ? conflict.getName() : MESSAGES.conflictWith(conflict.getName()), false));
					row.get(row.size() - 1).addStyleName("indent");
				} else {
					if (meeting.isArrangeHours()) {
						if (event.hasMessage())
							row.add(new Label(event.getMessage()));
						else
							row.add(new Label(CONSTANTS.arrangeHours()));
					} else {
						Label meetingDate = new Label(sDateFormatMeeting.format(meeting.getMeetingDate()), false);
						SessionMonth.Flag dateFlag = (iPropertiesProvider == null ? null : iPropertiesProvider.getDateFlag(event == null ? null : event.getType(), meeting.getMeetingDate()));
						if (dateFlag != null) {
							switch (dateFlag) {
							case FINALS:
								meetingDate.setTitle(MESSAGES.hintFinals());
								meetingDate.addStyleName("finals");
								break;
							case BREAK:
								meetingDate.setTitle(MESSAGES.hintBreak());
								meetingDate.addStyleName("break");
								break;
							case HOLIDAY:
								meetingDate.setTitle(MESSAGES.hintHoliday());
								meetingDate.addStyleName("holiday");
								break;
							case WEEKEND:
								meetingDate.setTitle(MESSAGES.hintWeekend());
								meetingDate.addStyleName("weekend");
								break;
							case MIDTERMS:
								meetingDate.setTitle(MESSAGES.hintMidterms());
								meetingDate.addStyleName("midterms");
								break;
							}
						}
						row.add(meetingDate);
					}
				}
				Label meetingTime = new Label(meeting.getMeetingTime(CONSTANTS), false);
				Label allocatedTime = new Label(meeting.getAllocatedTime(CONSTANTS), false);
				if (!meeting.isArrangeHours() && iPropertiesProvider != null && iPropertiesProvider.isTooEarly(meeting.getStartSlot(), meeting.getEndSlot())) {
					meetingTime.setTitle(MESSAGES.hintTooEarly());
					meetingTime.addStyleName("early");
					allocatedTime.setTitle(MESSAGES.hintTooEarly());
					allocatedTime.addStyleName("early");
				}
				if (meeting.isArrangeHours() && event.hasMessage()) {
					meetingTime = new Label(CONSTANTS.arrangeHours(), false);
				}
				row.add(meetingTime);
				row.add(allocatedTime);
				row.add(new NumberCell(meeting.getStartOffset()));
				row.add(new NumberCell(- meeting.getEndOffset()));
			}
			if (meeting.getLocation() == null) {
				if (data.hasParent() && data.getParent().hasMeeting() && data.getParent().getMeeting().getLocation() != null) {
					row.add(new HTML(data.getParent().getMeeting().getLocationNameWithHint(MESSAGES), false));
					row.add(new NumberCell(data.getParent().getMeeting().getLocation().getSize() == null ? MESSAGES.notApplicable() : data.getParent().getMeeting().getLocation().getSize().toString()));
				} else {
					row.add(new Label(""));
					row.add(new Label(""));
				}
			} else {
				row.add(new HTML(meeting.getLocationNameWithHint(MESSAGES), false));
				row.add(new NumberCell(meeting.getLocation().getSize() == null ? MESSAGES.notApplicable() : meeting.getLocation().getSize().toString()));
			}
			if (iShowMeetingContacts) {
				row.add(new HTML(meeting.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES), false));
				if (meeting.hasMeetingContacts() && !isColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_MEETING_CONTACTS) && getMode().hasFlag(ModeFlag.ShowOptionalColumns))
					setColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn(), true);
			} else {
				row.add(new HTML(""));
			}
			if (meeting.isPast() || (data.hasParent() && data.getParent().hasMeeting() && data.getParent().getMeeting().isPast()))
				for (int i = row.size() - 8; i < row.size(); i++)
					row.get(i).addStyleName("past-meeting");
			else if (meeting.hasStyle() || (data.hasParent() && data.getParent().hasMeeting() && data.getParent().getMeeting().hasStyle()))
				for (int i = row.size() - 8; i < row.size(); i++)
					row.get(i).addStyleName(meeting.getStyle());
		} else {
			String[] mtgs = new String[] {"", "", "", "", "", "", "", ""};
			String prevApproval = null;
			String[] prev = null;
			String prevSpan = null;
			allCancelledOrRejected = true;
			boolean globalUnavailability = event != null && event.getId() != null && event.getId() < 0 && event.getType() == EventType.Unavailabile;
			for (MultiMeetingInterface m: EventInterface.getMultiMeetings(data.getMeetings(getMeetingFilter()), true, globalUnavailability ? null : iPropertiesProvider, event == null ? null : event.getType(), iShowMeetingContacts)) {
				String[] mtg = new String[] {
						m.isArrangeHours() ? (event.hasMessage() ? event.getMessage() : CONSTANTS.arrangeHours()) : (m.getDays(iPropertiesProvider.getFirstDayOfWeek(), CONSTANTS) + " " + (m.getNrMeetings() == 1 ? sDateFormatLong.format(m.getFirstMeetingDate()) : sDateFormatShort.format(m.getFirstMeetingDate()) + " - " + sDateFormatLong.format(m.getLastMeetingDate()))),
						m.isArrangeHours() && event.hasMessage() ? CONSTANTS.arrangeHours() : m.getMeetings().first().getMeetingTime(CONSTANTS),
						m.getMeetings().first().getAllocatedTime(CONSTANTS),
						String.valueOf(m.getMeetings().first().getStartOffset()),
						String.valueOf(- m.getMeetings().first().getEndOffset()),
						m.getLocationNameWithHint(MESSAGES),
						(m.getMeetings().first().getLocation() == null ? "" : m.getMeetings().first().getLocation().hasSize() ? m.getMeetings().first().getLocation().getSize().toString() : MESSAGES.notApplicable()),
						iShowMeetingContacts ? m.getMeetingContacts(CONSTANTS.meetingContactsSeparator(), MESSAGES) : ""
						};
				if (!m.isArrangeHours() && !m.isPast()) {
					SessionMonth.Flag dateFlag = (globalUnavailability || iPropertiesProvider == null ? null : iPropertiesProvider.getDateFlag(event == null ? null : event.getType(), m.getFirstMeetingDate()));
					if (dateFlag != null) {
						switch (dateFlag) {
						case FINALS:
							mtg[0] = "<span class='finals' title=\"" + MESSAGES.hintFinals() + "\">" + mtg[0] + "</span>";
							break;
						case MIDTERMS:
							mtg[0] = "<span class='midterms' title=\"" + MESSAGES.hintMidterms() + "\">" + mtg[0] + "</span>";
							break;
						case BREAK:
							mtg[0] = "<span class='break' title=\"" + MESSAGES.hintBreak() + "\">" + mtg[0] + "</span>";
							break;
						case HOLIDAY:
							mtg[0] = "<span class='holiday' title=\"" + MESSAGES.hintHoliday() + "\">" + mtg[0] + "</span>";
							break;
						case WEEKEND:
							mtg[0] = "<span class='weekend' title=\"" + MESSAGES.hintWeekend() + "\">" + mtg[0] + "</span>";
							break;
						}
					}
				}
				if (!m.isArrangeHours() && iPropertiesProvider != null && iPropertiesProvider.isTooEarly(m.getMeetings().first().getStartSlot(), m.getMeetings().first().getEndSlot())) {
					for (int i = 1; i <= 2; i++)
						mtg[i] = "<span class='early' title=\"" + MESSAGES.hintTooEarly() + "\">" + mtg[i] + "</span>";
				}
				String span = "";
				if (m.getApprovalStatus() == ApprovalStatus.Cancelled)
					span = "cancelled-meeting";
				else if (m.getApprovalStatus() == ApprovalStatus.Rejected)
					span = "rejected-meeting";
				else if (m.isPast())
					span = "past-meeting";
				else if (m.hasStyle())
					span = m.getStyle();
				for (int i = 0; i < mtgs.length; i++) {
					mtgs[i] += (mtgs[i].isEmpty() ? "" : "<br>") + (prev != null && span.equals(prevSpan) && prev[i == 6 ? i - 1 : i].equals(mtg[i == 6 ? i - 1 : i]) && (i != 7 || !mtg[i].isEmpty()) ? MESSAGES.repeatingSymbol() : (!span.isEmpty() ? "<span class='" + span + "'>" : "") + mtg[i] + (!span.isEmpty() ? "</span>" : ""));
				}
				String thisApproval = (
						m.getApprovalStatus() == ApprovalStatus.Approved ? sDateFormatApproval.format(m.getApprovalDate()) :
						m.getApprovalStatus() == ApprovalStatus.Cancelled ? MESSAGES.approvalCancelled() :
						m.getApprovalStatus() == ApprovalStatus.Rejected ? MESSAGES.approvalRejected() :
						"");
							
				approval += (approval.isEmpty() ? "" : "<br>") + (prev != null && span.equals(prevSpan) && prevApproval.equals(thisApproval) ? MESSAGES.repeatingSymbol() : 
						(m.getApprovalStatus() == ApprovalStatus.Approved ?
						m.isPast() ? "<span class='past-meeting'>" + sDateFormatApproval.format(m.getApprovalDate()) + "</span>" : sDateFormatApproval.format(m.getApprovalDate()) :
						m.getApprovalStatus() == ApprovalStatus.Cancelled ? "<span class='cancelled-meeting'>" + MESSAGES.approvalCancelled() + "</span>":
						m.getApprovalStatus() == ApprovalStatus.Rejected ? "<span class='rejected-meeting'>" + MESSAGES.approvalRejected() + "</span>":
						event != null && event.getType() == EventType.Unavailabile ? "" : 
						m.getFirstMeetingDate() == null ? "" : m.isPast() ? "<span class='not-approved-past'>" + MESSAGES.approvalNotApprovedPast() + "</span>" :
						event != null && event.getExpirationDate() != null ? "<span class='not-approved'>" + MESSAGES.approvalExpire(sDateFormatExpiration.format(event.getExpirationDate())) + "</span>" : 
						"<span class='not-approved'>" + MESSAGES.approvalNotApproved() + "</span>"));
				if (EventCookie.getInstance().isHideDuplicitiesForMeetings()) {
					prev = mtg; prevSpan = span; prevApproval = thisApproval;
				}
				if (m.getApprovalStatus() != ApprovalStatus.Cancelled && m.getApprovalStatus() != ApprovalStatus.Rejected)
					allCancelledOrRejected = false;
				if (m.hasMeetingContacts() && !isColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_MEETING_CONTACTS) && getMode().hasFlag(ModeFlag.ShowOptionalColumns))
					setColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn(), true);
			}
			
			for (int i = 0; i < mtgs.length; i++) {
				if (i == 3 || i == 4 || i == 6)
					row.add(new NumberCell(mtgs[i]));
				else
					row.add(new HTML(mtgs[i], false));
			}
		}
		
		if (event != null && event.hasEnrollment()) {
			row.add(new NumberCell(event.getEnrollment().toString()));
		} else {
			row.add(new HTML("&nbsp;"));
		}
		
		if (event != null && event.hasMaxCapacity()) {
			row.add(new NumberCell(event.getMaxCapacity().toString()));
		} else {
			row.add(new HTML("&nbsp;"));
		}
		
		if (event != null && event.hasInstructors()) {
			if (getMode().hasFlag(ModeFlag.ShowEventDetails)) {
				List<String> names = new ArrayList<String>();
				for (ContactInterface instructor: event.getInstructors())
					names.add(instructor.getName(MESSAGES));
				row.add(new MultiLineStringCell(names));
			} else {
				row.add(new HTML(event.getInstructorNames("<br>", MESSAGES), false));
			}
		} else if (event != null && event.hasSponsor()) {
			row.add(new Label(event.getSponsor().getName()));
		} else {
			row.add(new HTML("&nbsp;"));
		}
		
		if (event != null && event.hasRequestedServices()) {
			if (getMode().hasFlag(ModeFlag.ShowEventDetails)) {
				List<String> names = new ArrayList<String>();
				for (EventServiceProviderInterface service: event.getRequestedServices())
					names.add(service.getLabel());
				row.add(new MultiLineStringCell(names));
			} else {
				row.add(new HTML(event.getRequestedServices("<br>"), false));
			}
			if (!isColumnVisible(getHeader(MESSAGES.colRequestedServices()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_REQUESTED_SERVICES) && getMode().hasFlag(ModeFlag.ShowEventDetails)) {
				setColumnVisible(getHeader(MESSAGES.colRequestedServices()).getColumn(), true);
			}
		} else {
			row.add(new HTML("&nbsp;"));
		}
		
		if (event != null && iShowMainContact) {
			row.add(new HTML(event.hasContact() ? event.getContact().getName(MESSAGES) : "&nbsp;"));
			if (!isColumnVisible(getHeader(MESSAGES.colMainContact()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_MAIN_CONTACT) && getMode().hasFlag(ModeFlag.ShowOptionalColumns)) {
				switch (event.getType()) {
				case Course:
				case Special:
					setColumnVisible(getHeader(MESSAGES.colMainContact()).getColumn(), true);
				}
			}
			
		} else {
			row.add(new HTML("&nbsp;"));
		}

		if (meeting != null) {
			boolean past = meeting.isPast() || (data.hasParent() && data.getParent().hasMeeting() && data.getParent().getMeeting().isPast());
			row.add(new HTML(
					conflict != null && (conflict.getType() == EventType.Unavailabile || conflict.getType() == EventType.Message) ? "" :
					meeting.getApprovalStatus() == ApprovalStatus.Deleted ? "<span class='deleted-meeting'>" + MESSAGES.approvalDeleted() + "</span>":
					meeting.getApprovalStatus() == ApprovalStatus.Cancelled ? "<span class='cancelled-meeting'>" + MESSAGES.approvalCancelled() + "</span>":
					meeting.getApprovalStatus() == ApprovalStatus.Rejected ? "<span class='rejected-meeting'>" + MESSAGES.approvalRejected() + "</span>":
					meeting.getMeetingDate() == null ? "" :
					meeting.getId() == null ? event != null && event.getType() == EventType.Unavailabile ? event.getId() != null && event.getId() < 0l ? "" : "<span class='new-meeting'>" + MESSAGES.approvalNewUnavailabiliyMeeting() + "</span>" :
					event != null && (event.getType() == EventType.Class || event.getType() == EventType.FinalExam || event.getType() == EventType.MidtermExam) ? "<span class='new-approved-meeting'>" + MESSAGES.approvelNewApprovedMeeting() + "</span>" :
					meeting.isAutomaticallyApproved() || (meeting.isCanApprove() && EventCookie.getInstance().isAutomaticallyApproveNewMeetings()) ? "<span class='new-approved-meeting'>" + MESSAGES.approvelNewApprovedMeeting() + "</span>" : "<span class='new-meeting'>" + MESSAGES.approvalNewMeeting() + "</span>" :
					meeting.isApproved() ? 
							past ? "<span class='past-meeting'>" + sDateFormatApproval.format(meeting.getApprovalDate()) + "</span>" : sDateFormatApproval.format(meeting.getApprovalDate()) :
							past ? "<span class='not-approved-past'>" + MESSAGES.approvalNotApprovedPast() + "</span>" : 
							event != null && event.getExpirationDate() != null ? "<span class='not-approved'>" + MESSAGES.approvalExpire(sDateFormatExpiration.format(event.getExpirationDate())) + "</span>" :
							"<span class='not-approved'>" + MESSAGES.approvalNotApproved() + "</span>"));
		} else {
			row.add(new HTML(approval == null ? "" : approval, false));
		}
		
		if (event != null && event.getLastNote() != null && iShowMainContact) {
			NoteInterface note = event.getLastNote();
			row.add(new Label(sDateFormatApproval.format(note.getDate()) + " " + note.getType().getName()));
		} else {
			row.add(new HTML("&nbsp;"));
		}

		if (!getMode().hasFlag(ModeFlag.MustShowApproval) && !isColumnVisible(getHeader(MESSAGES.colApproval()).getColumn()) && EventCookie.getInstance().get(EventFlag.SHOW_APPROVAL)) {
			if (event != null)
				switch (event.getType()) {
				case Course:
				case Special:
					setColumnVisible(getHeader(MESSAGES.colApproval()).getColumn(), true);
				}
		}
		
		int rowNumber = addRow(data, row);
		
		if (meeting != null)
			getRowFormatter().addStyleName(rowNumber,
					meeting.getApprovalStatus() == ApprovalStatus.Deleted ? "deleted-row" :
					meeting.getApprovalStatus() == ApprovalStatus.Cancelled ? "cancelled-row" :
					meeting.getApprovalStatus() == ApprovalStatus.Rejected ? "rejected-row" : "meeting-row");
		else if (event != null)
			getRowFormatter().addStyleName(rowNumber, allCancelledOrRejected ? "event-cancelled-row" : "event-row");
		
		if (data.hasParent()) {
			row.get(1).addStyleName("indent");
			if (conflict != null && conflict.getType() == EventType.Message) {
				getRowFormatter().addStyleName(rowNumber, "message-row");
				for (int i = 0; i < getCellCount(rowNumber); i++)
					getCellFormatter().addStyleName(rowNumber, i, "message-cell");
			} else {
				getRowFormatter().addStyleName(rowNumber, "conflict-row");
				for (int i = 0; i < getCellCount(rowNumber); i++)
					getCellFormatter().addStyleName(rowNumber, i, "conflict-cell");
			}
		} else if (meeting != null) {
			for (int i = 0; i < getCellCount(rowNumber); i++)
				getCellFormatter().addStyleName(rowNumber, i, "meeting-cell");
		} else if (event != null) {
			for (int i = 0; i < getCellCount(rowNumber); i++)
				getCellFormatter().addStyleName(rowNumber, i, "event-cell");
		}

		if (!data.hasParent()) {
			if (meeting != null) {
				if (meeting.getApprovalStatus() == ApprovalStatus.Pending ||  meeting.getApprovalStatus() == ApprovalStatus.Approved) {
					if (meeting.hasConflicts()) {
						for (MeetingConflictInterface cMeeting: meeting.getConflicts())
							add(new EventMeetingRow(null, cMeeting, data));
					} else if (event.hasConflicts()) {
						for (EventInterface cEvent: event.getConflicts()) {
							if (cEvent.hasMeetings())
								for (MeetingInterface cMeeting: cEvent.getMeetings()) {
									if (meeting.inConflict(cMeeting))
										add(new EventMeetingRow(cEvent, cMeeting, data));
								}
						}
					}
				}
			} else if (event != null) {
				if (event.hasConflicts())
					for (EventInterface cEvent: event.getConflicts())
						add(new EventMeetingRow(cEvent, null, data));
			}
		}
	}
	
	private Set<Integer> getEventColumns() {
		Set<Integer> cols = new HashSet<Integer>();
		cols.add(getHeader(MESSAGES.colName()).getColumn());
		cols.add(getHeader(MESSAGES.colSection()).getColumn());
		cols.add(getHeader(MESSAGES.colTitle()).getColumn());
		cols.add(getHeader(MESSAGES.colNote()).getColumn());
		cols.add(getHeader(MESSAGES.colType()).getColumn());
		cols.add(getHeader(MESSAGES.colEnrollment()).getColumn());
		cols.add(getHeader(MESSAGES.colLimit()).getColumn());
		cols.add(getHeader(MESSAGES.colSponsorOrInstructor()).getColumn());
		cols.add(getHeader(MESSAGES.colRequestedServices()).getColumn());
		cols.add(getHeader(MESSAGES.colMainContact()).getColumn());
		cols.add(getHeader(MESSAGES.colLastChange()).getColumn());
		return cols;
	}
	
	public void resetColumnVisibility() {
		if (getRowCount() <= 1) {
			setColumnVisible(0, false);
			setColumnVisible(getHeader(MESSAGES.colSection()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colTitle()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colNote()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colMainContact()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colApproval()).getColumn(), getMode().hasFlag(ModeFlag.MustShowApproval));
			setColumnVisible(getHeader(MESSAGES.colLastChange()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colRequestedServices()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn(), false);
		}
		setColumnVisible(getHeader(MESSAGES.colName()).getColumn(), getMode().hasFlag(ModeFlag.ShowEventDetails));
		setColumnVisible(getHeader(MESSAGES.colType()).getColumn(), getMode().hasFlag(ModeFlag.ShowEventDetails));
		if (getMode().hasFlag(ModeFlag.ShowOptionalColumns)) {
			setColumnVisible(getHeader(MESSAGES.colPublishedTime()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_PUBLISHED_TIME));
			setColumnVisible(getHeader(MESSAGES.colAllocatedTime()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_ALLOCATED_TIME));
			setColumnVisible(getHeader(MESSAGES.colSetupTimeShort()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_SETUP_TIME));
			setColumnVisible(getHeader(MESSAGES.colTeardownTimeShort()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_TEARDOWN_TIME));
			setColumnVisible(getHeader(MESSAGES.colLimit()).getColumn(), getMode().hasFlag(ModeFlag.ShowEventDetails) && EventCookie.getInstance().get(EventFlag.SHOW_LIMIT));
			setColumnVisible(getHeader(MESSAGES.colEnrollment()).getColumn(), getMode().hasFlag(ModeFlag.ShowEventDetails) && EventCookie.getInstance().get(EventFlag.SHOW_ENROLLMENT));
			setColumnVisible(getHeader(MESSAGES.colCapacity()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_CAPACITY));
			setColumnVisible(getHeader(MESSAGES.colSponsorOrInstructor()).getColumn(), getMode().hasFlag(ModeFlag.ShowEventDetails) && EventCookie.getInstance().get(EventFlag.SHOW_SPONSOR));
			setColumnVisible(getHeader(MESSAGES.colLastChange()).getColumn(), isShowMainContact() && getMode().hasFlag(ModeFlag.ShowEventDetails) && EventCookie.getInstance().get(EventFlag.SHOW_LAST_CHANGE));
		} else {
			setColumnVisible(getHeader(MESSAGES.colPublishedTime()).getColumn(), EventCookie.getInstance().get(EventFlag.SHOW_PUBLISHED_TIME));
			setColumnVisible(getHeader(MESSAGES.colAllocatedTime()).getColumn(), !EventCookie.getInstance().get(EventFlag.SHOW_PUBLISHED_TIME));
			setColumnVisible(getHeader(MESSAGES.colSetupTimeShort()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colTeardownTimeShort()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colLimit()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colEnrollment()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colCapacity()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colSponsorOrInstructor()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colRequestedServices()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colMainContact()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colTitle()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colNote()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colApproval()).getColumn(), getMode().hasFlag(ModeFlag.MustShowApproval));
			setColumnVisible(getHeader(MESSAGES.colLastChange()).getColumn(), false);
			setColumnVisible(getHeader(MESSAGES.colMeetingContacts()).getColumn(), false);
		}
	}
	
	public boolean hasSortBy() { return iSortBy != null; }
	public String getSortBy() { return iSortBy == null ? null : (iAsc ? "" : "-") + iSortBy.ordinal(); }
	public void setSortBy(String sortBy) {
		if (sortBy == null || sortBy.isEmpty()) {
			iSortBy = null;
		} else if (sortBy.startsWith("+")) {
			iAsc = true;
			iSortBy = EventMeetingSortBy.values()[Integer.parseInt(sortBy.substring(1))];
		} else if (sortBy.startsWith("-")) {
			iAsc = false;
			iSortBy = EventMeetingSortBy.values()[Integer.parseInt(sortBy.substring(1))];
		} else {
			iAsc = true;
			iSortBy = (sortBy == null ? null : EventMeetingSortBy.values()[Integer.parseInt(sortBy)]);	
		}
		sort();
	}
	
	protected void addSortByOperation(final UniTimeTableHeader header, final EventMeetingSortBy sortBy) {
		header.addOperation(new Operation() {
			@Override
			public void execute() {
				if (header.getOrder() != null)
					iAsc = !header.getOrder();
				else
					iAsc = true;
				iSortBy = sortBy;
				sort(); onSortByChanded(sortBy, iAsc);
			}
			@Override
			public boolean isApplicable() { return getRowCount() > 1; }
			@Override
			public boolean hasSeparator() { return true; }
			@Override
			public String getName() { return MESSAGES.opSortBy(header.getHTML()); }
		});
	}
	
	protected void onSortByChanded(EventMeetingSortBy sortBy, boolean asc) {};
	
	protected void onColumnShownOrHid(int eventCookieFlags) {}

	protected Operation addHideOperation(final UniTimeTableHeader header, final EventFlag flag) {
		return addHideOperation(header, flag, null);
	}
	
	protected Operation addHideOperation(final UniTimeTableHeader header, final EventFlag flag, final Check separator) {
		Operation op = new AriaOperation() {
			@Override
			public void execute() {
				boolean visible = isColumnVisible(header.getColumn());
				setColumnVisible(header.getColumn(), !visible);
				EventCookie.getInstance().set(flag, !visible);
				if (flag == EventFlag.SHOW_ALLOCATED_TIME && visible) {
					UniTimeTableHeader other = getHeader(MESSAGES.colPublishedTime());
					if (!isColumnVisible(other.getColumn())) {
						setColumnVisible(other.getColumn(), true);
						EventCookie.getInstance().set(EventFlag.SHOW_PUBLISHED_TIME, true);
					}
				} else if (flag == EventFlag.SHOW_PUBLISHED_TIME && visible) {
					UniTimeTableHeader other = getHeader(MESSAGES.colAllocatedTime());
					if (!isColumnVisible(other.getColumn())) {
						setColumnVisible(other.getColumn(), true);
						EventCookie.getInstance().set(EventFlag.SHOW_ALLOCATED_TIME, true);
					}
				}
				onColumnShownOrHid(EventCookie.getInstance().getFlags());
			}
			@Override
			public boolean isApplicable() {
				switch (flag) {
				case SHOW_MAIN_CONTACT:
				case SHOW_LAST_CHANGE:
					return iShowMainContact && getMode().hasFlag(ModeFlag.ShowEventDetails);
				case SHOW_LIMIT:
				case SHOW_ENROLLMENT:
				case SHOW_SPONSOR:
				case SHOW_NOTE:
				case SHOW_REQUESTED_SERVICES:
					return getMode().hasFlag(ModeFlag.ShowEventDetails);
				case SHOW_TITLE:
					return isColumnVisible(getHeader(MESSAGES.colSection()).getColumn());
				case SHOW_APPROVAL:
					return !getMode().hasFlag(ModeFlag.MustShowApproval);
				case SHOW_MEETING_CONTACTS:
					return iShowMeetingContacts;
				default:
					return true;
				}
			}
			@Override
			public boolean hasSeparator() { 
				return separator != null && separator.isChecked();
			}
			@Override
			public String getName() { return isColumnVisible(header.getColumn()) ? MESSAGES.opHide(header.getHTML()) : MESSAGES.opShow(header.getHTML()); }
			@Override
			public String getAriaLabel() { return isColumnVisible(header.getColumn()) ? ARIA.opHide(header.getHTML()) : ARIA.opShow(header.getHTML()); }
		};
		getHeader(null).addOperation(op);
		getHeader(MESSAGES.colName()).addOperation(ifNotSelectable(op));
		switch (flag) {
		case SHOW_ALLOCATED_TIME:
		case SHOW_PUBLISHED_TIME:
		case SHOW_SETUP_TIME:
		case SHOW_TEARDOWN_TIME:
			getHeader(MESSAGES.colAllocatedTime()).addOperation(op);
			getHeader(MESSAGES.colPublishedTime()).addOperation(op);
			getHeader(MESSAGES.colSetupTimeShort()).addOperation(op);
			getHeader(MESSAGES.colTeardownTimeShort()).addOperation(op);
			break;
		case SHOW_CAPACITY:
		case SHOW_LIMIT:
		case SHOW_ENROLLMENT:
			getHeader(MESSAGES.colLocation()).addOperation(op);
			header.addOperation(op);
			break;
		case SHOW_MAIN_CONTACT:
		case SHOW_LAST_CHANGE:
			getHeader(MESSAGES.colApproval()).addOperation(op);
			header.addOperation(op);
			break;
		case SHOW_SPONSOR:
			getHeader(MESSAGES.colApproval()).addOperation(op);
			header.addOperation(op);
			break;
		case SHOW_TITLE:
			getHeader(MESSAGES.colSection()).addOperation(op);
			getHeader(MESSAGES.colType()).addOperation(op);
			header.addOperation(op);
			break;
		default:
			header.addOperation(op);
		}
		return op;
	}
	
	private Operation ifNotSelectable(final Operation op) {
		return new Operation() {
			@Override
			public void execute() {
				op.execute();
			}
			@Override
			public String getName() {
				return op.getName();
			}
			@Override
			public boolean isApplicable() {
				return op.isApplicable() && !isColumnVisible(getHeader(null).getColumn());
			}
			@Override
			public boolean hasSeparator() {
				return op.hasSeparator();
			}
		};
	}
	
	public void sort() {
		if (iSortBy != null) {
			UniTimeTableHeader header = null;
			switch (iSortBy) {
			case NAME:
				header = getHeader(MESSAGES.colName()); 
				break;
			case SECTION:
				header = getHeader(MESSAGES.colSection()); 
				break;
			case TYPE:
				header = getHeader(MESSAGES.colType()); 
				break;
			case DATE:
				header = getHeader(MESSAGES.colDate()); 
				break;
			case PUBLISHED_TIME:
				header = getHeader(MESSAGES.colPublishedTime()); 
				break;
			case ALLOCATED_TIME:
				header = getHeader(MESSAGES.colAllocatedTime()); 
				break;
			case SETUP_TIME:
				header = getHeader(MESSAGES.colSetupTimeShort()); 
				break;
			case TEARDOWN_TIME:
				header = getHeader(MESSAGES.colTeardownTimeShort()); 
				break;
			case LOCATION:
				header = getHeader(MESSAGES.colLocation()); 
				break;
			case CAPACITY:
				header = getHeader(MESSAGES.colCapacity()); 
				break;
			case SPONSOR:
				header = getHeader(MESSAGES.colSponsorOrInstructor()); 
				break;
			case MAIN_CONTACT:
				header = getHeader(MESSAGES.colMainContact()); 
				break;
			case APPROVAL:
				header = getHeader(MESSAGES.colApproval()); 
				break;
			case LIMIT:
				header = getHeader(MESSAGES.colLimit()); 
				break;
			case ENROLLMENT:
				header = getHeader(MESSAGES.colEnrollment()); 
				break;
			case TITLE:
				header = getHeader(MESSAGES.colTitle()); 
				break;
			case NOTE:
				header = getHeader(MESSAGES.colNote()); 
				break;
			case LAST_CHANGE:
				header = getHeader(MESSAGES.colLastChange()); 
				break;
			case MEETING_CONTACTS:
				header = getHeader(MESSAGES.colMeetingContacts());
				break;
			case SERVICES:
				header = getHeader(MESSAGES.colRequestedServices()); 
				break;
			}
			sort(header, new Comparator<EventMeetingRow>() {
				@Override
				public int compare(EventMeetingRow o1, EventMeetingRow o2) {
					int cmp = compareRows(o1.hasParent() ? o1.getParent() : o1, o2.hasParent() ? o2.getParent() : o2);
					if (cmp != 0) return cmp;
					return compareRows(o1.hasParent() ? o1 : null, o2.hasParent() ? o2 : null);
				}
			}, iAsc);
		}
		if (getMode().hasFlag(ModeFlag.ShowMeetings) && getMode().hasFlag(ModeFlag.ShowEventDetails)) {
			Long eventId = null, conflictId = null;
			Set<Integer> eventCols = getEventColumns();
			int line = 0;
			for (int row = 1; row < getRowCount(); row++) {
				EventMeetingRow data = getData(row);
				EventMeetingRow next = (row + 1 < getRowCount() ? getData(row + 1) : null);
				boolean hasNext = (next != null && data.hasParent() == next.hasParent() && data.getEventId().equals(next.getEventId()));
				if (!data.hasParent()) {
					if (eventId == null || !eventId.equals(data.getEventId())) {
						line = 0;
						for (int col = 0; col < getCellCount(row); col++)
							getCellFormatter().addStyleName(row, col, "event-cell");
						for (int col: eventCols) {
							Widget w = getWidget(row, col);
							if (w instanceof MultiLineCell)
								((MultiLineCell)w).showLine(line, hasNext);
							else
								w.setVisible(true);
						}
					} else {
						for (int col = 0; col < getCellCount(row); col++)
							getCellFormatter().removeStyleName(row, col, "event-cell");
						for (int col: eventCols) {
							Widget w = getWidget(row, col);
							if (w instanceof MultiLineCell)
								((MultiLineCell)w).showLine(line, hasNext);
							else
								w.setVisible(false);
						}
					}
				} else {
					if (conflictId == null || !conflictId.equals(data.getEventId())) {
						line = 0;
						for (int col: eventCols) {
							Widget w = getWidget(row, col);
							if (w instanceof MultiLineCell)
								((MultiLineCell)w).showLine(line, hasNext);
							else
								w.setVisible(true);
						}
					} else {
						for (int col: eventCols) {
							Widget w = getWidget(row, col);
							if (w instanceof MultiLineCell)
								((MultiLineCell)w).showLine(line, hasNext);
							else
								w.setVisible(false);
						}
					}
				}
				eventId = (data.hasParent() ? null : data.getEventId());
				conflictId = (data.hasParent() ? data.getEventId() : null);
				line++;
			}
		}
	}
	
	protected int compareRows(EventMeetingRow r1, EventMeetingRow r2) {
		// Null first
		if (r1 == null) return (r2 == null ? 0 : -1);
		if (r2 == null) return 1;
		
		// Compare event properties (if applicable)
		if (r1.hasEvent() && r2.hasEvent()) {
			int cmp = EventComparator.compareEvents(r1.getEvent(), r2.getEvent(), iSortBy);
			if (cmp != 0) return cmp;
		}
		
		// Compare meeting properties (if applicable)
		if (r1.hasMeeting() && r2.hasMeeting()) {
			int cmp = EventComparator.compareMeetings(r1.getMeeting(), r2.getMeeting(), iSortBy);
			if (cmp != 0) return cmp;
		} else {
			Iterator<MeetingInterface> i1 = r1.getMeetings(getMeetingFilter()).iterator(), i2 = r2.getMeetings(getMeetingFilter()).iterator();
			while (i1.hasNext() && i2.hasNext()) {
				int cmp = EventComparator.compareMeetings(i1.next(), i2.next(), iSortBy);
				if (cmp != 0) return cmp;
			}
		}
		
		// Fallback 1
		if (r1.hasEvent() && r2.hasEvent()) {
			int cmp = EventComparator.compareFallback(r1.getEvent(), r2.getEvent());
			if (cmp != 0) return cmp;
		}
		if (r1.hasMeeting() && r2.hasMeeting()) {
			int cmp = EventComparator.compareFallback(r1.getMeeting(), r2.getMeeting());
			if (cmp != 0) return cmp;
		} else {
			Iterator<MeetingInterface> i1 = r1.getMeetings(getMeetingFilter()).iterator(), i2 = r2.getMeetings(getMeetingFilter()).iterator();
			while (i1.hasNext() && i2.hasNext()) {
				int cmp = EventComparator.compareFallback(i1.next(), i2.next());
				if (cmp != 0) return cmp;
			}
			if (i1.hasNext() && !i2.hasNext()) return 1;
			if (!i1.hasNext() && i2.hasNext()) return -1;
		}
		
		// Fallback 2
		if (r1.hasMeeting() && r2.hasMeeting()) {
			return r1.getMeeting().compareTo(r2.getMeeting());
		} else  if (r1.hasEvent() && r2.hasEvent()) {
			return r1.getEvent().compareTo(r2.getEvent());
		} else {
			return 0;
		}
	}
	
	protected abstract class EventMeetingOperation implements Operation {
		@Override
		public void execute() {
			if (hasSelection()) {
				for (int row = getRowCount() - 1; row >= 1; row--) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (ch.getValue()) {
							EventMeetingRow e = getData(row);
							if (isApplicable(e)) execute(row, e);
						}
					}
				}
			} else if (allowNoSelection()) {
				for (int row = getRowCount() - 1; row >= 1; row--) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						EventMeetingRow e = getData(row);
						if (isApplicable(e)) execute(row, e);
					}
				}
			}
		}

		public boolean hasSelection() {
			for (int row = 1; row < getRowCount(); row++) {
				Widget w =  getWidget(row, 0);
				if (w != null && w instanceof CheckBox) {
					CheckBox ch = (CheckBox)w;
					if (ch.getValue()) return true;
				}
			}
			return false;
		}

		@Override
		public boolean isApplicable() {
			boolean hasSelection = hasSelection();
			boolean allMustMatch = allMustMatch(hasSelection);
			if (hasSelection) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (ch.getValue()) {
							EventMeetingRow e = getData(row);
							if (isApplicable(e)) {
								if (!allMustMatch) return true;
							} else {
								if (allMustMatch) return false;
							}
						}
					}
				}
				return allMustMatch;
			} else if (allowNoSelection()) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						EventMeetingRow e = getData(row);
						if (isApplicable(e)) {
							if (!allMustMatch) return true;
						} else {
							if (allMustMatch) return false;
						}
					}
				}
				return allMustMatch;
			} else {
				return false;
			}
		}
		
		public List<EventMeetingRow> data() {
			List<EventMeetingRow> data = new ArrayList<EventMeetingRow>();
			if (hasSelection()) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (ch.getValue()) {
							EventMeetingRow e = getData(row);
							if (isApplicable(e)) data.add(e);
						}
					}
				}
			} else if (allowNoSelection()) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						EventMeetingRow e = getData(row);
						if (isApplicable(e)) data.add(e);
					}
				}
			}
			return data;
		}
		
		public List<Integer> rows() {
			List<Integer> rows = new ArrayList<Integer>();
			if (hasSelection()) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						CheckBox ch = (CheckBox)w;
						if (ch.getValue()) {
							EventMeetingRow e = getData(row);
							if (isApplicable(e)) rows.add(row);
						}
					}
				}
			} else if (allowNoSelection()) {
				for (int row = 1; row < getRowCount(); row++) {
					Widget w =  getWidget(row, 0);
					if (w != null && w instanceof CheckBox) {
						EventMeetingRow e = getData(row);
						if (isApplicable(e)) rows.add(row);
					}
				}
			}
			return rows;
		}
		
		public boolean allMustMatch(boolean hasSelection) {
			return hasSelection;
		}
		
		public boolean allowNoSelection() {
			return true;
		}
		
		public abstract boolean isApplicable(EventMeetingRow event);
		public abstract void execute(int row, EventMeetingRow event);

		@Override
		public boolean hasSeparator() { return false; }
	}
	
	public EventInterface next(Long eventId) {
		boolean next = false;
		for (int row = 1; row < getRowCount(); row ++) {
			EventMeetingRow data = getData(row);
			if (!data.hasParent()) {
				if (next && !eventId.equals(data.getEventId()))
					return data.getEvent();
				else if (eventId.equals(data.getEvent().getId()))
					next = true;
			}
		}
		return null;
	}
	
	public EventInterface previous(Long eventId) {
		EventInterface prev = null;
		for (int row = 1; row < getRowCount(); row ++) {
			EventMeetingRow data = getData(row);
			if (!data.hasParent()) {
				if (eventId.equals(data.getEventId()))
					return prev;
				else
					prev = data.getEvent();
			}
		}
		return null;
	}

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

	@Override
	public List<EventMeetingRow> getValue() {
		List<EventMeetingRow> value = new ArrayList<EventMeetingRow>();
		for (int row = 1; row < getRowCount(); row++) {
			EventMeetingRow data = getData(row);
			if (data != null && !data.hasParent()) value.add(data);
		}
		return value;
	}
	
	public List<MeetingInterface> getMeetings() {
		List<MeetingInterface> meetings = new ArrayList<MeetingInterface>();
		for (int row = 1; row < getRowCount(); row++) {
			EventMeetingRow data = getData(row);
			if (data != null && data.hasMeeting() && !data.hasParent()) meetings.add(data.getMeeting());
		}
		return meetings;
	}
	
	public List<EventInterface> getEvents() {
		List<EventInterface> events = new ArrayList<EventInterface>();
		for (int row = 1; row < getRowCount(); row++) {
			EventMeetingRow data = getData(row);
			if (data != null && data.hasEvent() && !data.hasParent()) events.add(data.getEvent());
		}
		return events;
	}
	
	public Map<EventInterface, List<MeetingInterface>> getEventMeetings() {
		Map<EventInterface, List<MeetingInterface>> event2meetings = new HashMap<EventInterface, List<MeetingInterface>>();
		for (int row = 1; row < getRowCount(); row++) {
			EventMeetingRow data = getData(row);
			if (data == null || data.hasParent() || !data.hasEvent()) continue;
			List<MeetingInterface> meetings = event2meetings.get(data.getEvent());
			if (meetings == null) {
				meetings = new ArrayList<MeetingInterface>();
				event2meetings.put(data.getEvent(), meetings);
			}
			if (data.hasMeeting())
				meetings.add(data.getMeeting());
			else
				meetings.addAll(data.getMeetings(getMeetingFilter()));
		}
		return event2meetings;
	}

	@Override
	public void setValue(List<EventMeetingRow> value) {
		setValue(value, false);
	}

	@Override
	public void setValue(List<EventMeetingRow> value, boolean fireEvents) {
		clearTable(1);
		resetColumnVisibility();
		if (value != null)
			for (EventMeetingRow row: value)
				add(row);
		sort();
		if (fireEvents)
			ValueChangeEvent.fire(this, value);
	}
	
	public void setEvents(Collection<EventInterface> events) {
		setEvents(events, false);
	}
	
	public void setEvents(Collection<EventInterface> events, boolean fireEvents) {
		List<EventMeetingRow> rows = new ArrayList<EventMeetingTable.EventMeetingRow>();
		if (events != null) {
			for (EventInterface event: events) {
				if (getMode().hasFlag(ModeFlag.ShowMeetings)) {
					for (MeetingInterface meeting: event.getMeetings())
						if (getMeetingFilter() == null || !getMeetingFilter().filter(event, meeting))
							rows.add(new EventMeetingRow(event, meeting));
				} else {
					rows.add(new EventMeetingRow(event, null));
				}
			}
		}
		setValue(rows, fireEvents);
	}
	
	public void setMeetings(EventInterface event, Collection<MeetingInterface> meetings) {
		setMeetings(event, meetings, false);
	}
	
	public void setMeetings(EventInterface event, Collection<MeetingInterface> meetings, boolean fireEvents) {
		List<EventMeetingRow> rows = new ArrayList<EventMeetingTable.EventMeetingRow>();
		if (getMode().hasFlag(ModeFlag.ShowMeetings)) {
			for (MeetingInterface meeting: meetings)
				rows.add(new EventMeetingRow(event, meeting));
		} else {
			rows.add(new EventMeetingRow(event, null));
		}
		setValue(rows, fireEvents);
	}

	public static class EventMeetingRow {
		EventInterface iEvent;
		MeetingInterface iMeeting;
		EventMeetingRow iParent;
		
		EventMeetingRow(EventInterface event, MeetingInterface meeting, EventMeetingRow parent) {
			iEvent = event; iMeeting = meeting; iParent = parent;
		}
		EventMeetingRow(EventInterface event, MeetingInterface meeting) {
			this(event, meeting, null);
		}
		
		public boolean hasEvent() { return iEvent != null; }
		public EventInterface getEvent() { return iEvent; }
		public Long getEventId() {
			if (iEvent != null) return iEvent.getId();
			if (iMeeting != null && iMeeting instanceof MeetingConflictInterface)
				return ((MeetingConflictInterface)iMeeting).getEventId();
			return null;
		}
		
		public boolean hasMeeting() { return iMeeting != null; }
		public MeetingInterface getMeeting() {
			if (iMeeting != null && iMeeting.getId() == null) {
				if (iMeeting.isAutomaticallyApproved() || (EventCookie.getInstance().isAutomaticallyApproveNewMeetings() && iMeeting.isCanApprove()))
					iMeeting.setApprovalStatus(ApprovalStatus.Approved);
				else
					iMeeting.setApprovalStatus(ApprovalStatus.Pending);
			}
			return iMeeting;
		}
		
		public boolean hasParent() { return iParent != null; }
		public EventMeetingRow getParent() { return iParent; }
		
		public boolean inConflict() {
			if (iMeeting != null && iMeeting.inConflict()) return true;
			if (iEvent != null && iEvent.hasConflicts()) {
				if (iMeeting == null) return true;
				for (EventInterface conflict: iEvent.getConflicts()) {
					if (conflict.getType() == EventType.Message) continue;
					if (conflict.inConflict(iMeeting)) return true;
				}
			}
			return false;
		}
		
		public boolean isCanApprove() {
			if (iMeeting != null) return iMeeting.isCanApprove();
			if (iEvent != null && iEvent.hasMeetings())
				for (MeetingInterface meeting: iEvent.getMeetings())
					if (meeting.isCanApprove()) return true;
			return false;
		}
		
		public boolean isCanInquire() {
			if (iMeeting != null) return iMeeting.isCanInquire();
			if (iEvent != null && iEvent.hasMeetings())
				for (MeetingInterface meeting: iEvent.getMeetings())
					if (meeting.isCanInquire()) return true;
			return false;
		}

		public boolean isCanCancel() {
			if (iMeeting != null) return iMeeting.isCanCancel();
			if (iEvent != null && iEvent.hasMeetings())
				for (MeetingInterface meeting: iEvent.getMeetings())
					if (meeting.isCanCancel()) return true;
			return false;
		}
		
		public boolean isCanDelete() {
			if (iMeeting != null) return iMeeting.isCanDelete();
			if (iEvent != null && iEvent.hasMeetings())
				for (MeetingInterface meeting: iEvent.getMeetings())
					if (meeting.isCanDelete()) return true;
			return false;
		}

		public boolean isEditable() {
			return iMeeting != null && (iMeeting.getId() == null || iMeeting.isCanEdit());
		}
		
		public List<MeetingInterface> getMeetings(MeetingFilter filter) {
			List<MeetingInterface> meetings = new ArrayList<MeetingInterface>();
			if (iMeeting != null) {
				if (filter == null || !filter.filter(iParent == null ? iEvent : iParent.iEvent, iMeeting)) meetings.add(iMeeting);
			} else if (iEvent != null && iEvent.hasMeetings()) {
				for (MeetingInterface meeting: iEvent.getMeetings())
					if (filter == null || !filter.filter(iParent == null ? iEvent : iParent.iEvent, meeting)) meetings.add(meeting);
			}
			return meetings;
		}
		
		public String toAriaString(boolean includeEventInfo) {
			String label = "";
			if (includeEventInfo && hasEvent()) {
				label += (getEvent().getType() == null || getEvent().getType() == EventType.Unavailabile ? "" : getEvent().getType().getAbbreviation(CONSTANTS));
				if (getEvent().getName() != null)
					label += " " + getEvent().getName();
			}
			if (hasMeeting() && getMeeting().getMeetingDate() != null) {
				String room = "", hint = "";
				if (getMeeting().getLocation() != null) {
					room = getMeeting().getLocation().getName();
					if (getMeeting().getLocation().hasRoomType())
						hint = getMeeting().getLocation().getRoomType();
				}
				label += ARIA.dateTimeRoomSelection(sDateFormatAria.format(getMeeting().getMeetingDate()), TimeUtils.slot2aria(getMeeting().getStartSlot()), TimeUtils.slot2aria(getMeeting().getEndSlot()), room, hint);
			}
			return label;
		}
	}
	
	private static interface MultiLineCell {
		public void showLine(int line, boolean hasNext);
	}
	
	private static class MultiLineStringCell extends HTML implements MultiLineCell {
		List<String> iValue;
		
		public MultiLineStringCell(List<String> value) {
			super();
			setWordWrap(false);
			iValue = value;
			showLine(0, false);
		}
		
		public MultiLineStringCell(String... values) {
			super();
			setWordWrap(false);
			iValue = new ArrayList<String>();
			for (String value: values)
				iValue.add(value);
			showLine(0, false);
		}
		
		public void showLine(int line, boolean hasNext) {
			String html = (line >= 0 && line < iValue.size() ? iValue.get(line) : "");
			if (!hasNext && line >= 0 && line + 1 < iValue.size()) {
				for (int i = line + 1; i < iValue.size(); i++)
					html += "<br>" + iValue.get(i);
			}
			setHTML(html);
		}
	}
	
	private static class MultiLinePCell extends P implements MultiLineCell {
		List<P> iValue;
		
		public MultiLinePCell(List<P> value) {
			super("multiple-lines");
			iValue = value;
			showLine(0, false);
		}
		
		public MultiLinePCell(P... values) {
			super("multiple-lines");
			iValue = new ArrayList<P>();
			for (P value: values)
				iValue.add(value);
			showLine(0, false);
		}
		
		public void showLine(int line, boolean hasNext) {
			clear();
			add(line >= 0 && line < iValue.size() ? iValue.get(line) : new P("blank-line"));
			if (!hasNext && line >= 0 && line + 1 < iValue.size()) {
				for (int i = line + 1; i < iValue.size(); i++)
					add(iValue.get(i));
			}
		}
	}
	
	private static class MultiLineNumberCell extends MultiLineStringCell implements HasCellAlignment {
		public MultiLineNumberCell(List<String> value) {
			super(value);
		}
		@Override
		public HorizontalAlignmentConstant getCellAlignment() {
			return HasHorizontalAlignment.ALIGN_RIGHT;
		}
	}
	
	public interface MeetingFilter {
		public boolean filter(EventInterface event, MeetingInterface meeting);
	}
	
	public interface Implementation {
		public void execute(EventMeetingTable source, OperationType operation, List<EventMeetingRow> selection);
	}
	
	public interface Check {
		public boolean isChecked();
	}
	
	public static class HTMLWithColSpan extends HTML implements UniTimeTable.HasColSpan {
		private int iColspan = 1;
		public HTMLWithColSpan(String html, boolean wordWrap, int colspan) {
			super(html, wordWrap);
			iColspan = colspan;
		}
		@Override
		public int getColSpan() { return iColspan; }
	}
	
	private class CourseName extends P {
		private CourseDetailsRpcResponse iDetails = null;

		public CourseName(final Long id, String name, String... styles) {
			super(styles);
			setText(name);
			if (id != null) {
				addMouseOverHandler(new MouseOverHandler() {
					@Override
					public void onMouseOver(MouseOverEvent event) {
						if (iDetails == null) {
							RPC.execute(new CourseDetailsRpcRequest(id), new AsyncCallback<CourseDetailsRpcResponse>() {
								@Override
								public void onFailure(Throwable caught) {
									iDetails = new CourseDetailsRpcResponse();
								}

								@Override
								public void onSuccess(CourseDetailsRpcResponse result) {
									iDetails = result; showDetails();
								}
							});
						} else {
							showDetails();
						}
					}
				});
				addMouseOutHandler(new MouseOutHandler() {
					@Override
					public void onMouseOut(MouseOutEvent event) {
						hideDetails();
					}
				});
				addClickHandler(new ClickHandler() {
					@Override
					public void onClick(ClickEvent event) {
						if (iDetails == null) {
							RPC.execute(new CourseDetailsRpcRequest(id), new AsyncCallback<CourseDetailsRpcResponse>() {
								@Override
								public void onFailure(Throwable caught) {
									iDetails = new CourseDetailsRpcResponse();
								}

								@Override
								public void onSuccess(CourseDetailsRpcResponse result) {
									iDetails = result; openLink();
								}
							});
						} else {
							openLink();
						}
						event.stopPropagation();
					}
				});
			}
		}
		
		protected void showDetails() {
			if (iDetails != null && iDetails.hasDetails()) {
				HTML details = new HTML(iDetails.getDetails());
				details.setStyleName("unitime-CourseDetailsPopup");
				GwtHint.showHint(getElement(), details);
			}
		}
		
		protected void hideDetails() {
			if (iDetails != null && iDetails.hasDetails())
				GwtHint.hideHint();
		}
		
		protected void openLink() {
			if (iDetails != null && iDetails.hasLink()) {
				clearHover();
				GwtHint.hideHint();
				UniTimeFrameDialog.openDialog(MESSAGES.courseCatalogDialog(getText()), iDetails.getLink(), null, null, false);
			}
		}
	}
	
}
