(function($) {
	$.fullCalendar.views.rhc_year = YearView;

	function YearView(element, calendar) {
		var t = this;

		// exports
		t.render = render;

		calendar.options.rhc_year = 'undefined' === typeof calendar.options.rhc_year ? {} : calendar.options.rhc_year;

		// imports
		BasicYearView.call(t, element, calendar, 'rhc_year');
		var opt = t.opt;
		var renderYear = t.renderYear;
		var formatDate = calendar.formatDate;

		function render(date, delta) {
			var firstMonth = opt('firstMonth') || 0;
			var lastMonth = opt('lastMonth') || firstMonth+12;
			var nbMonths = lastMonth - firstMonth;
			var dateRange = cloneDate(date, true);
			dateRange.setFullYear(date.getFullYear(),lastMonth,0, 12);
			if (delta) {
				addYears(date, delta);
			}
			// for school years month to year navigation
			else if (firstMonth > 0 && date.getMonth() <= dateRange.getMonth()) {
				addYears(date, -1);
			}
			var start = cloneDate(date, true);
			start.setFullYear(start.getFullYear(),firstMonth,1, 12);
			var end = cloneDate(date);
			end.setFullYear(end.getFullYear(),lastMonth,0, 12);

			var monthsPerRow = parseInt( calendar.options.rhc_year.columns ); //ex: '2x6', '3x4', '4x3'

			t.title = formatDate(start, opt('titleFormat'));
			if (firstMonth + nbMonths > 12) {
				t.title += formatDate(end, ' - yyyy');
			}

			t.start = start;
			t.end = end;

			renderYear(monthsPerRow, true);
		}
	}

	function BasicYearView(element, calendar, viewName) {
		var t = this;

		// exports
		t.renderYear = renderYear;
		t.setHeight = setHeight;
		t.setWidth = setWidth;
		t.renderDayOverlay = renderDayOverlay;
		t.defaultSelectionEnd = defaultSelectionEnd;
		t.renderSelection = renderSelection;
		t.clearSelection = clearSelection;
		t.reportDayClick = reportDayClick; // for selection (kinda hacky)
		t.defaultEventEnd = defaultEventEnd;
		t.getHoverListener = function() { return hoverListener; };
		t.colContentLeft = colContentLeft;
		t.colContentRight = colContentRight;
		t.colLeft = colLeft;
		t.colRight = colRight;
		t.dayOfWeekCol = dayOfWeekCol;
		t.dateCell = dateCell;
		t.allDayRow = allDayRow;
		t.allDayBounds = allDayBounds;
		t.getIsCellAllDay = function() { return true; };
		t.getRowCnt = function() { return rowCnt; };
		t.getColCnt = function() { return colCnt; };
		t.getColWidth = function() { return colWidth; };
		t.getBodyRows = function() { return bodyRows; };
		t.getDaySegmentContainer = function() { return daySegmentContainer; };
		t.getRowMaxWidth = getRowMaxWidth;

		View.call(t, element, calendar, viewName);
		OverlayManager.call(t);
		SelectionManager.call(t);

		t.rangeToSegments = rangeToSegmentsYear;
		BasicEventRenderer.call(t);

		t.rowToGridOffset = rowToGridOffset;
		t.cellToCellOffset = cellToCellOffset;

		t.dragStart = dragStart;
		t.dragStop  = dragStop;

		// overrides
		var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
		t.cellOffsetToDayOffset = cellOffsetToDayOffsetYear;
		var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
		t.dayOffsetToCellOffset = dayOffsetToCellOffsetYear;
		var changeView = calendar.changeView;
		calendar.changeView = changeViewFromYear;

		// imports
		var opt = t.opt;
		var trigger = t.trigger;
		var clearEvents = t.clearEvents;
		var renderOverlay = t.renderOverlay;
		var clearOverlays = t.clearOverlays;
		var isHiddenDay = t.isHiddenDay;
		var daySelectionMousedown = t.daySelectionMousedown;
		var formatDate = calendar.formatDate;
		var cellToDate = t.cellToDate;

		// locals
		var table;
		var bodyRows;

		var subTables;

		var bodyCells;
		var bodyCellTopInners;
		var daySegmentContainer;
		var refreshCoordGrids;

		var viewWidth;
		var colWidth;

		var rowCnt, colCnt;
		var coordinateGrids = [];
		var coordinateGrid;
		var hoverListener;
		var colContentPositions;
		var otherMonthDays = [];
		var rowsForMonth = [];

		var rtl, dis, dit;
		var firstDay;
		var firstMonth;
		var lastMonth;
		var hiddenMonths;
		var nwe;
		var tm;
		var colFormat;
		var yearCellMinH;
		var showWeekNumbers;
		var weekNumberTitle;
		var weekNumberFormat;


		/* Rendering
		------------------------------------------------------------*/

		disableTextSelection(element.addClass('fc-grid'));

		function renderYear(yearColumns, showNumbers) {
			updateOptions();
			setCalendarBounds();

			var firstTime = !table;
			if (!firstTime) {
				clearEvents();
				table.remove();
			}

			var monthsPerRow = parseInt(yearColumns,10); //"3x4" parses to 3
			buildSkeleton(monthsPerRow, showNumbers);
			updateCells();
			opt('debug') && debugCellOffsets();
			buildCoordGrids();
		}

		function updateOptions() {
			firstDay = opt('firstDay') || 0;
			firstMonth = opt('firstMonth') || 0;
			lastMonth = opt('lastMonth') || firstMonth+12;
			hiddenMonths = opt('hiddenMonths') || [];
			yearCellMinH = opt('yearCellMinH') || 20;
			colFormat = opt('columnFormat') || 'MMMM';
			showWeekNumbers = opt('weekNumbers');
			weekNumberTitle = opt('weekNumberTitle');
			weekNumberFormat = opt('weekNumberCalculation') != 'iso' ? "w" : "W";

			nwe = opt('weekends') ? 0 : 1;
			rtl = opt('isRTL');
			tm = opt('theme') ? 'ui' : 'fc';

			colCnt = nwe ? 5 : 7;
			if (rtl) {
				dis = -1;
				dit = colCnt - 1;
			} else {
				dis = 1;
				dit = 0;
			}
		}

		function setCalendarBounds() {

			var visStart = cloneDate(t.start);
			var visEnd = cloneDate(t.end);

			startOfWeek(visStart);

			addDays(visEnd, 14);
			startOfWeek(visEnd);
			t.skipHiddenDays(visEnd, -1, true);

			t.visStart = visStart;
			t.visEnd = visEnd;
		}

		function buildSkeleton(monthsPerRow, showNumbers) {
			var s;
			var headerClass = tm + "-widget-header";
			var contentClass = tm + "-widget-content";
			var head, headCells;
			var i, j, m, n, y, monthsRow = 0;
			var monthName, dayStr;
			var di = cloneDate(t.start);
			var miYear = di.getFullYear();
			var nbMonths = lastMonth-firstMonth;
			var overlayHtml = '';

			rowCnt = 0;
			var localWeekNames = [];
			// init days based on 2013-12 (1st is Sunday)
			for (m=0; m<7; m++) {
				di.setFullYear(2013,11,1+m, 12);
				localWeekNames[m] = formatDate(di, 'ddd');
			}
			di = cloneDate(t.start);
			s = '<div class="rhc-year-view rhc-month-wrap" data-columns="'+calendar.options.rhc_year.columns+'">';
			s += '<td class="fc-year-month-border fc-first"></td>';
			n = 0;
			for (m=firstMonth; m<lastMonth; m++) {

				var hiddenMonth = ($.inArray(m,hiddenMonths) != -1);
				var display = (hiddenMonth ? 'display:none;' : '');

				di.setFullYear(miYear,m,1, 12);
				y = di.getFullYear();
				monthName = formatDate(di, 'MMMM');
				if (firstMonth + nbMonths > 12) {
					monthName = monthName + ' ' + y;
				}

				startOfWeek(di);

				s +='<td class="fc-year-monthly-td" style="' + display + '">';
				s +='<div class="rhc-month">' +
					'<h3 class="month-title">'+htmlEscape(monthName)+'</h3>' +
					'<table class="fc-border-separate" cellspacing="0">'+
					'<thead>'+
					'<tr>';


				if (showWeekNumbers) {
					s += "<th class='fc-week-number " + headerClass + "'>" +
						htmlEscape(weekNumberTitle) +
						"</th>";
				}

				for (i=firstDay; i<firstDay+7; i++) {
					if (nwe && (i%7 === 0 || i%7 === 6)) {
						continue;
					}
					// need fc- for setDayID
					s += '<th class="fc-day-header fc-'+dayIDs[i%7]+' '+headerClass+'" width="'+((100/colCnt)|0)+'%">'+
					 localWeekNames[i%7]+'</th>';
				}
				s += '</tr></thead><tbody>';

				rowsForMonth[m] = 0;
				for (i=0; i<6; i++) {
					if (nwe) { skipWeekend(di); }
					// don't show week if all days are in next month
					if (di.getMonth() == (m+1)%12 && opt('weekMode') != 'fixed') {
						continue;
					}
					rowsForMonth[m]++;
					rowCnt++;

					s += '<tr class="fc-week fc-week' + i + '">';
					if (showWeekNumbers) {
						s +=
							"<td class='fc-week-number " + contentClass + "'>" +
							"<div>" +
							htmlEscape(formatDate(di, weekNumberFormat)) +
							"</div>" +
							"</td>";
					}
					for (j=0; j<colCnt; j++) {
						if (di.getMonth() == (m%12)) {
							dayStr=formatDate(di, '-yyyy-MM-dd');
						} else {
							dayStr='';
						}

						s += '<td class="'+contentClass+' fc-day fc-'+dayIDs[di.getDay()]+' fc-day'+dayStr + '">' + // need fc- for setDayID
						'<div>' +
							(showNumbers ? '<div class="fc-day-number"/>' : '') +
							'<div class="fc-day-content" style="min-height:'+yearCellMinH+'px;">' +
								'<div class="fc-day-content-date">'+di+'</div>' +
								'<div class="fc-day-content-overlay">' +
									'<div class="fc-day-content-heading">'+formatDate(di, calendar.options.rhc_year.overlay_date_format)+'</div>' +
									'<div class="fc-day-content-events"></div>' +
								'</div>' +
							'</div>' +
						'</div>' +
						'</td>';

						addDays(di, 1);
						if (nwe) { skipWeekend(di); }
					}
					s += '</tr>';
				}
				s += '</tbody></table></div>';
				s += '</td>';

				if (!hiddenMonth) { n++; }
			}
			s += '<td class="fc-year-month-border fc-last"></td>';
			s += '</div>';

			table = $(s).appendTo(element);
			head = table.find('thead');
			headCells = head.find('th.fc-day-header');

			bodyRows = table.find('table tbody tr');
			bodyCells = bodyRows.find('td').not('.fc-year-monthly-td');

			subTables = table.find('table');
			subTables.find('tbody .fc-week0').addClass('fc-first');
			subTables.find('tbody > tr:last').addClass('fc-last');

			bodyCellTopInners = subTables.find('tbody .fc-week0 .fc-day-content div');

			markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
			markFirstLast(bodyRows);

			table.find('.fc-year-monthly-name a').click(function() {
				calendar.changeView('month');
				calendar.gotoDate($(this).attr('data-year'), $(this).attr('data-month'), 15);
			});

			dayBind(bodyCells);
			daySegmentContainer = $('<div style="position:absolute;z-index:8;top:0;left:0;display:none;"/>').appendTo(element);

			if ( 'overlay' === calendar.options.rhc_year.click_action ) {
				var convertedBg = hexToRgb(calendar.options.rhc_year.overlay_background);
				var bgColorProperty = 'background-color:rgba('+convertedBg.r+','+convertedBg.g+','+convertedBg.b+','+calendar.options.rhc_year.overlay_opacity/100+');';

				overlayHtml += '<div class="eyv-overlay eyv-overlay-'+calendar.options.rhc_year.overlay_animation+'" style="'+bgColorProperty+'">';
				overlayHtml += '<button type="button" class="eyv-overlay-close"><i class="eyv-overlay-close-icon"></i></button>';
				overlayHtml += '<div class="eyv-overlay-content"></div></div>';

				if ( 0 === $( '.eyv-overlay' ).length ) {
					$('body').append(overlayHtml).find('.eyv-overlay-close').on('click', function() {
						$(this).closest('.eyv-overlay').removeClass('open').addClass('close');
					});
				}
			}
		}

		/**
		 * Compute otherMonthDays, set fc-today and day numbers
		 */
		function updateCells() {
			var today = clearTime(new Date());

			var d = cloneDate(t.start);
			var miYear = d.getFullYear();

			subTables.each(function(i, _sub) {

				var lastDateShown = 0;
				var mi = i+firstMonth;

				d.setFullYear(miYear,mi,1, 12);
				startOfWeek(d);

				otherMonthDays[mi] = [0,0,-1,-1];
				otherMonthDays[mi+1] = [0,0,-1,-1];

				$(_sub).find('tbody > tr').each(function(iii, _tr) {

					$(_tr).find('td').not('.fc-week-number').each(function(ii, _cell) {

						var cell = $(_cell);

						if (!dateInMonth(d,mi)) {
							cell.addClass('fc-other-month');
							if (d.getMonth() == (mi+11)%12) {
								// prev month
								otherMonthDays[mi][0]++;
							} else {
								// next month
								otherMonthDays[mi][1]++;
							}
						} else {
							if (otherMonthDays[mi][2] < 0) {
								// first in current month, hidden days at start
								otherMonthDays[mi][2] = d.getDate()-1;
							}
							lastDateShown = d.getDate();
						}
						if (+d == +today) {
							cell.addClass(tm + '-state-highlight fc-today');
						} else {
							cell.addClass((+d < +today) ? 'fc-past' : 'fc-future');
						}
						var dayNumber = d.getDate();
						cell.find('div.fc-day-number').text(dayNumber);

						addDays(d, 1);
						if (nwe) { skipWeekend(d); }
					});
				});

				var endDaysHidden = daysInMonth(t.start.getFullYear(), mi+1) - lastDateShown;
				// in current month, but hidden (weekends) at end
				otherMonthDays[mi][3] = endDaysHidden;

			});
			bodyRows.filter('.fc-year-have-event').removeClass('fc-year-have-event');
		}

		/**
		 * Show computed offsets to debug
		 */
		function debugCellOffsets() {
			var today = clearTime(new Date());

			var d = cloneDate(t.start);
			var miYear = d.getFullYear();

			subTables.each(function(i, _sub) {

				var lastDateShown = 0;
				var mi = i+firstMonth;

				d.setFullYear(miYear,mi,1, 12);
				startOfWeek(d);

				$(_sub).find('tbody > tr').each(function(iii, _tr) {

					$(_tr).find('td').not('.fc-week-number').each(function(ii, _cell) {

						var cell = $(_cell);

						var dayNumber = d.getDate();
						var dayOffset = t.dateToDayOffset(d);
						var cellOffset = dayOffsetToCellOffsetYear(dayOffset);
						dayNumber = dayNumber+"\n"+cellOffset+"-"+dayOffset+"/"+cellOffsetToDayOffsetYear(cellOffset);
						cell.find('div.fc-day-number').text(dayNumber);

						addDays(d, 1);
						if (nwe) { skipWeekend(d); }
					});
				});
			});
			bodyRows.filter('.fc-year-have-event').removeClass('fc-year-have-event');
		}

		function setHeight(height) {
			//not sure what supposed to do
		}

		function setWidth(width) {
			viewWidth = width;
			colContentPositions.clear();
		}


		/* Day clicking and binding
		-----------------------------------------------------------*/

		function dayBind(days) {
			days.click(dayClick).mousedown(daySelectionMousedown);
		}

		function dayClick(ev) {
			if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
				var match = this.className.match(/fc\-day\-(\d+)\-(\d+)\-(\d+)/);
				if (match != null) {
					var date = new Date(match[1], match[2]-1, match[3]);
					trigger('dayClick', this, date, true, ev);
				}
			}
		}


		/* Semi-transparent Overlay Helpers
		------------------------------------------------------*/

		function renderDayOverlay(overlayStart, overlayEnd) { // overlayEnd is exclusive

			if (!isFinite(overlayStart))
				return;

			var segments = t.rangeToSegments(overlayStart, overlayEnd);
			for (var i=0; i<segments.length; i++) {
				var segment = segments[i];
				var grid = coordinateGrids[segment.gridOffset];
				var gridRow = rowToGridRow(segment.row);
				dayBind(
					renderCellOverlay(
						grid,
						gridRow,
						segment.leftCol,
						gridRow,
						segment.rightCol
					)
				);
			}
			return;
		}

		function renderCellOverlay(grid, row0, col0, row1, col1) { // row1,col1 is inclusive
			var rect = grid.rect(row0, col0, row1, col1, element);
			return renderOverlay(rect, element);
		}

		/* used ? */
		function getRowMaxWidth(row) {
			return $(subTables[(row/5)|0]).width();
		}

		/* Selection
		-----------------------------------------------------------------------*/

		function defaultSelectionEnd(startDate, allDay) {
			return cloneDate(startDate);
		}

		function renderSelection(startDate, endDate, allDay) {
			renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
		}

		function clearSelection() {
			clearOverlays();
		}

		function reportDayClick(date, allDay, ev) {
			var cell = dateCell(date);
			var _element = bodyCells[cell.row*colCnt + cell.col];
			trigger('dayClick', _element, date, allDay, ev);
		}


		/* External Dragging
		-----------------------------------------------------------------------*/
		function dragStart(_dragElement, ev, ui) {
			hoverListener.start(function(cell) {
				clearOverlays();
				if (cell) {
					renderCellOverlay(cell.grid, cell.row, cell.col, cell.row, cell.col);
				}
			}, ev);
		}

		function dragStop(_dragElement, ev, ui) {
			var cell = hoverListener.stop();
			clearOverlays();
			if (cell) {
				var offset = t.cellToCellOffset(cell);
				var dayOffset = t.cellOffsetToDayOffset(offset);
				var d = t.dayOffsetToDate(dayOffset);
				trigger('drop', _dragElement, d, true, ev, ui);
			}
		}

		/* Utilities
		--------------------------------------------------------*/
		function cellsForMonth(i) {
			return rowsForMonth[i] * (nwe ? 5 : 7);
		}

		/* decrease date until start of week */
		function startOfWeek(d) {
			while (d.getDay() != firstDay) {
				addDays(d,-1);
			}
			if (nwe) { skipWeekend(d); }
		}

		function skipWeekend(date, inc, excl) {
			inc = inc || 1;
			while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) {
				addDays(date, inc);
			}
			return date;
		}

		/* main function for event's position */
		function dayOffsetToCellOffsetYear(dOffset) {
			var i, j, offset = 0;
			var dayOffset = dOffset - otherMonthDays[firstMonth][0];
			if (dOffset < otherMonthDays[firstMonth][0]) {
				return dayOffsetToCellOffset(dOffset);
			}

			for (i=firstMonth; i<lastMonth; i++) {

				var moDays = daysInMonth(t.start.getFullYear(), i+1);
				var di = new Date(t.start.getFullYear(),i,1, 12,0);

				if (dayOffset < moDays || i == lastMonth-1) {

					offset += otherMonthDays[i][0]; //days in other month at beginning of month;

					for (j = 0; j < dayOffset; j++) {
						if (!nwe || !isHiddenDay(di)) {
							offset += 1;
						}
						addDays(di, 1);
					}
					return offset;
				}
				dayOffset -= moDays;
				offset += cellsForMonth(i);
			}
		}

		function cellToCellOffset(row, col, gridOffset) {

			// rtl variables. wish we could pre-populate these. but where?
			var dis = rtl ? -1 : 1;
			var dit = rtl ? colCnt - 1 : 0;

			if (typeof(row) == 'object') {
				gridOffset = row.grid.offset;
				col = row.col;
				row = row.row;
			}

			var offset = 0;
			for (var i = 0; i < (gridOffset || 0); i++) {
				offset += cellsForMonth(i+firstMonth);
			}

			offset += row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)

			return offset;
		}

		/* handle selectable days clicks */
		function cellOffsetToDayOffsetYear(c) {
			var cellOffset = c;
			var offset = otherMonthDays[firstMonth][0];
			if (c < offset) {
				return cellOffsetToDayOffset(c);
			}

			var monthFirst=0;

			for (var i=firstMonth; i<lastMonth; i++) {
				var moDays = daysInMonth(t.start.getFullYear(), i+1);
				var moCellDays = cellsForMonth(i);

				if (cellOffset < moCellDays) {

					cellOffset -= otherMonthDays[i][0];
					cellOffset += otherMonthDays[i][2];
					var di = new Date(t.start.getFullYear(),i,1, 12);

					while (cellOffset > 0 || (nwe && isHiddenDay(di))) {
						if (!nwe || !isHiddenDay(di)) {
							cellOffset -= 1;
						}
						addDays(di, 1);
						offset += 1;
					}
					return offset;
				}

				monthFirst += moDays;
				cellOffset -= moCellDays;
				offset += moDays;
			}
		}

		// required to fix events on last month day
		function rangeToSegmentsYear(startDate, endDate) {
			//var rowCnt = t.getRowCnt();
			//var colCnt = t.getColCnt();
			var segments = []; // array of segments to return

			var realEnd = cloneDate(endDate);
			addDays(realEnd,-1);

			// ignore events outside current view
			if (realEnd < t.visStart || startDate > t.visEnd) {
				return segments;
			}

			// day offset for given date range
			var rangeDayOffsetStart = t.dateToDayOffset(startDate);
			var rangeDayOffsetEnd = t.dateToDayOffset(endDate); // exclusive

			// if ends in weekend, dont create a new segment
			if (nwe && isHiddenDay(realEnd)) {
				skipWeekend(realEnd,-1);
				addDays(realEnd,1);
				rangeDayOffsetEnd = t.dateToDayOffset(realEnd);
			}

			// first and last cell offset for the given date range
			// "last" implies inclusivity
			var rangeCellOffsetFirst = t.dayOffsetToCellOffset(rangeDayOffsetStart);
			var rangeCellOffsetLast = t.dayOffsetToCellOffset(rangeDayOffsetEnd - 1);

			var isStart, isEnd = false;

			// loop through all the rows in the view
			for (var row=0; row<rowCnt; row++) {

				// first and last cell offset for the row
				var rowCellOffsetFirst = row * colCnt;
				var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;

				// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
				var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
				var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);

				// make sure segment's offsets are valid and in view
				if (segmentCellOffsetFirst <= segmentCellOffsetLast) {

					var gridOffset = t.rowToGridOffset(row);
					var gridRow = rowToGridRow(row);
					var lenBefore = segmentCellOffsetFirst - rangeCellOffsetFirst;
					var skipSegment = false;

					// segment in next month could begin before start date
					if (!gridRow && segments.length && lenBefore >= 0 && lenBefore <= 14
					    && otherMonthDays[gridOffset+firstMonth][0] > 1) {
						segmentCellOffsetFirst = Math.min(rangeCellOffsetFirst, rowCellOffsetFirst);
					}

					// translate to cells
					var segmentCellFirst = t.cellOffsetToCell(segmentCellOffsetFirst);
					var segmentCellLast = t.cellOffsetToCell(segmentCellOffsetLast);

					// view might be RTL, so order by leftmost column
					var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();

					// Determine if segment's first/last cell is the beginning/end of the date range.
					// We need to compare "day offset" because "cell offsets" are often ambiguous and
					// can translate to multiple days, and an edge case reveals itself when we the
					// range's first cell is hidden (we don't want isStart to be true).
					var segmentDayOffsetLast = t.cellOffsetToDayOffset(segmentCellOffsetLast);

					// segment in end of month could ends after real end
					while (segmentDayOffsetLast >= rangeDayOffsetEnd) {
						cols[1]--; segmentDayOffsetLast--;
						if (cols[1] < cols[0]) {
							skipSegment = true;
							break;
						}
					}

					isStart = t.cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
					isEnd = segmentDayOffsetLast + 1 == rangeDayOffsetEnd; // +1 (exclusively)

					// segment in hidden month
					if ($.inArray(firstMonth+gridOffset,hiddenMonths) != -1) {
						skipSegment = true;
					}

					// we could enhance this, hiding segments on hiddendays
					if (!skipSegment && gridOffset>=0) {
						segments.push({
							gridOffset: gridOffset,
							row: row,
							leftCol: cols[0],
							rightCol: cols[1],
							isStart: isStart,
							isEnd: isEnd
						});
					}
				}
			}
			return segments;
		}

		function daysInMonth(year, month) {
			return new Date(year, month, 0).getDate();
		}

		function dateInMonth(date, mi) {
			var y = date.getFullYear() - t.start.getFullYear();
			return (date.getMonth() == mi-(y*12));
		}

		// grid number of row
		function rowToGridOffset(row) {
			var cnt = 0, ret = -1;
			for (var i=firstMonth; i<lastMonth; i++) {
				cnt += rowsForMonth[i];
				if (row < cnt) {
					ret = i-firstMonth;
					break;
				}
			}
			return ret;
		}

		// row index in grid
		function rowToGridRow(row) {
			var cnt = 0, ret = -1;
			for (var i=firstMonth; i<lastMonth; i++) {
				cnt += rowsForMonth[i];
				if (row < cnt) {
					ret = row-(cnt-rowsForMonth[i]);
					break;
				}
			}
			return ret;
		}

		function defaultEventEnd(event) {
			return cloneDate(event.start);
		}

		function tableByOffset(offset) {
			return $(subTables[offset]);
		}

		function buildCoordGrids() {
			var nums = [];
			for (var i=firstMonth; i<lastMonth; i++) {
				nums.push(i);
			}
			coordinateGrids = [];
			$.each(nums, function(offset, m) {
				var grid = new CoordinateGrid(function(rows, cols) {
					var _subTable = tableByOffset(offset);
					var _head = _subTable.find('thead');
					var _headCells = _head.find('th.fc-day-header');
					var _bodyRows = _subTable.find('tbody tr');

					var e, n, p;
					_headCells.each(function(i, _e) {
						e = $(_e);
						n = e.offset().left;
						p = [n, n+e.outerWidth()];
						cols[i] = p;
					});
					_bodyRows.each(function(i, _e) {
						if (i < rowCnt) {
							e = $(_e);
							n = e.offset().top;
							p = [n, n+e.outerHeight()];
							rows[i] = p;
						}
					});
				});
				grid.offset = offset;
				coordinateGrids.push(grid);
			});
			t.refreshCoordGrids = false;

			hoverListener = new HoverListener(coordinateGrids);
		}

		colContentPositions = new HorizontalPositionCache(function(col) {
			return bodyCellTopInners.eq(col);
		});

		function colContentLeft(col, gridOffset) {
			// var grid = tableByOffset(gridOffset);
			var grid = tableByOffset(col); // gridOffset is undefined
			return colContentPositions.left(col) + grid.position().left;
		}

		function colContentRight(col, gridOffset) {
			// var grid = tableByOffset(gridOffset);
			var grid = tableByOffset(col); // gridOffset is undefined
			return colContentPositions.right(col) + grid.position().left;
		}

		function colLeft(col, gridOffset) {
			return colContentLeft(col, gridOffset);
		}

		function colRight(col, gridOffset) {
			return colContentRight(col, gridOffset);
		}

		function dateCell(date) {
			return {
				row: Math.floor(dayDiff(date, t.visStart) / 7),
				col: dayOfWeekCol(date.getDay())
			};
		}

		function dayOfWeekCol(dayOfWeek) {
			return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
		}

		function allDayRow(i) {
			return bodyRows.eq(i);
		}

		function allDayBounds(i) {
			return {
				left: 0,
				right: viewWidth
			};
		}

		// overrides Agenda function to set current date from school view (multi-year)
		// else a click to 'month/week' buttons could use the previous year
		function changeViewFromYear(newViewName) {
			var currentView = calendar.getView().name;
			changeView(newViewName);
			if (firstMonth > 0 && currentView == 'year' && newViewName != currentView) {
				var today = clearTime(new Date());
				calendar.gotoDate(today);
			}
		}

	}

	function View(element, calendar, viewName) {
		var t = this;
		
		
		// exports
		t.element = element;
		t.calendar = calendar;
		t.name = viewName;
		t.opt = opt;
		t.trigger = trigger;
		t.isEventDraggable = isEventDraggable;
		t.isEventResizable = isEventResizable;
		t.setEventData = setEventData;
		t.clearEventData = clearEventData;
		t.eventEnd = eventEnd;
		t.reportEventElement = reportEventElement;
		t.triggerEventDestroy = triggerEventDestroy;
		t.eventElementHandlers = eventElementHandlers;
		t.showEvents = showEvents;
		t.hideEvents = hideEvents;
		t.eventDrop = eventDrop;
		t.eventResize = eventResize;
		// t.title
		// t.start, t.end
		// t.visStart, t.visEnd
		
		
		// imports
		var defaultEventEnd = t.defaultEventEnd;
		var normalizeEvent = calendar.normalizeEvent; // in EventManager
		var reportEventChange = calendar.reportEventChange;
		
		
		// locals
		var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
		var eventElementsByID = {}; // eventID mapped to array of jQuery elements
		var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
		var options = calendar.options;
		
		
		
		function opt(name, viewNameOverride) {
			var v = options[name];
			if ($.isPlainObject(v)) {
				return smartProperty(v, viewNameOverride || viewName);
			}
			return v;
		}

		
		function trigger(name, thisObj) {
			return calendar.trigger.apply(
				calendar,
				[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
			);
		}
		


		/* Event Editable Boolean Calculations
		------------------------------------------------------------------------------*/

		
		function isEventDraggable(event) {
			var source = event.source || {};
			return firstDefined(
					event.startEditable,
					source.startEditable,
					opt('eventStartEditable'),
					event.editable,
					source.editable,
					opt('editable')
				)
				&& !opt('disableDragging'); // deprecated
		}
		
		
		function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
			var source = event.source || {};
			return firstDefined(
					event.durationEditable,
					source.durationEditable,
					opt('eventDurationEditable'),
					event.editable,
					source.editable,
					opt('editable')
				)
				&& !opt('disableResizing'); // deprecated
		}
		
		
		
		/* Event Data
		------------------------------------------------------------------------------*/
		
		
		function setEventData(events) { // events are already normalized at this point
			eventsByID = {};
			var i, len=events.length, event;
			for (i=0; i<len; i++) {
				event = events[i];
				if (eventsByID[event._id]) {
					eventsByID[event._id].push(event);
				}else{
					eventsByID[event._id] = [event];
				}
			}
		}


		function clearEventData() {
			eventsByID = {};
			eventElementsByID = {};
			eventElementCouples = [];
		}
		
		
		// returns a Date object for an event's end
		function eventEnd(event) {
			return event.end ? cloneDate(event.end) : defaultEventEnd(event);
		}
		
		
		
		/* Event Elements
		------------------------------------------------------------------------------*/
		
		
		// report when view creates an element for an event
		function reportEventElement(event, element) {
			eventElementCouples.push({ event: event, element: element });
			if (eventElementsByID[event._id]) {
				eventElementsByID[event._id].push(element);
			}else{
				eventElementsByID[event._id] = [element];
			}
		}


		function triggerEventDestroy() {
			$.each(eventElementCouples, function(i, couple) {
				t.trigger('eventDestroy', couple.event, couple.event, couple.element);
			});
		}
		
		
		// attaches eventClick, eventMouseover, eventMouseout
		function eventElementHandlers(event, eventElement) {
			eventElement
				.click(function(ev) {
					if (!eventElement.hasClass('ui-draggable-dragging') &&
						!eventElement.hasClass('ui-resizable-resizing')) {
							return trigger('eventClick', this, event, ev);
						}
				})
				.hover(
					function(ev) {
						trigger('eventMouseover', this, event, ev);
					},
					function(ev) {
						trigger('eventMouseout', this, event, ev);
					}
				);
			// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
			// TODO: same for resizing
		}
		
		
		function showEvents(event, exceptElement) {
			eachEventElement(event, exceptElement, 'show');
		}
		
		
		function hideEvents(event, exceptElement) {
			eachEventElement(event, exceptElement, 'hide');
		}
		
		
		function eachEventElement(event, exceptElement, funcName) {
			// NOTE: there may be multiple events per ID (repeating events)
			// and multiple segments per event
			var elements = eventElementsByID[event._id],
				i, len = elements.length;
			for (i=0; i<len; i++) {
				if (!exceptElement || elements[i][0] != exceptElement[0]) {
					elements[i][funcName]();
				}
			}
		}
		
		
		
		/* Event Modification Reporting
		---------------------------------------------------------------------------------*/
		
		
		function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
			var oldAllDay = event.allDay;
			var eventId = event._id;
			moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
			trigger(
				'eventDrop',
				e,
				event,
				dayDelta,
				minuteDelta,
				allDay,
				function() {
					// TODO: investigate cases where this inverse technique might not work
					moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
					reportEventChange(eventId);
				},
				ev,
				ui
			);
			reportEventChange(eventId);
		}
		
		
		function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
			var eventId = event._id;
			elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
			trigger(
				'eventResize',
				e,
				event,
				dayDelta,
				minuteDelta,
				function() {
					// TODO: investigate cases where this inverse technique might not work
					elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
					reportEventChange(eventId);
				},
				ev,
				ui
			);
			reportEventChange(eventId);
		}
		
		
		
		/* Event Modification Math
		---------------------------------------------------------------------------------*/
		
		
		function moveEvents(events, dayDelta, minuteDelta, allDay) {
			minuteDelta = minuteDelta || 0;
			for (var e, len=events.length, i=0; i<len; i++) {
				e = events[i];
				if (allDay !== undefined) {
					e.allDay = allDay;
				}
				addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
				if (e.end) {
					e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
				}
				normalizeEvent(e, options);
			}
		}
		
		
		function elongateEvents(events, dayDelta, minuteDelta) {
			minuteDelta = minuteDelta || 0;
			for (var e, len=events.length, i=0; i<len; i++) {
				e = events[i];
				e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
				normalizeEvent(e, options);
			}
		}



		// ====================================================================================================
		// Utilities for day "cells"
		// ====================================================================================================
		// The "basic" views are completely made up of day cells.
		// The "agenda" views have day cells at the top "all day" slot.
		// This was the obvious common place to put these utilities, but they should be abstracted out into
		// a more meaningful class (like DayEventRenderer).
		// ====================================================================================================


		// For determining how a given "cell" translates into a "date":
		//
		// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
		//    Keep in mind that column indices are inverted with isRTL. This is taken into account.
		//
		// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
		//
		// 3. Convert the "day offset" into a "date" (a JavaScript Date object).
		//
		// The reverse transformation happens when transforming a date into a cell.


		// exports
		t.isHiddenDay = isHiddenDay;
		t.skipHiddenDays = skipHiddenDays;
		t.getCellsPerWeek = getCellsPerWeek;
		t.dateToCell = dateToCell;
		t.dateToDayOffset = dateToDayOffset;
		t.dayOffsetToCellOffset = dayOffsetToCellOffset;
		t.cellOffsetToCell = cellOffsetToCell;
		t.cellToDate = cellToDate;
		t.cellToCellOffset = cellToCellOffset;
		t.cellOffsetToDayOffset = cellOffsetToDayOffset;
		t.dayOffsetToDate = dayOffsetToDate;
		t.rangeToSegments = rangeToSegments;


		// internals
		var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
		var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
		var cellsPerWeek;
		var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
		var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
		var isRTL = opt('isRTL');


		// initialize important internal variables
		(function() {

			if (opt('weekends') === false) {
				hiddenDays.push(0, 6); // 0=sunday, 6=saturday
			}

			// Loop through a hypothetical week and determine which
			// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
			for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
				dayToCellMap[dayIndex] = cellIndex;
				isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
				if (!isHiddenDayHash[dayIndex]) {
					cellToDayMap[cellIndex] = dayIndex;
					cellIndex++;
				}
			}

			cellsPerWeek = cellIndex;
			if (!cellsPerWeek) {
				throw 'invalid hiddenDays'; // all days were hidden? bad.
			}

		})();


		// Is the current day hidden?
		// `day` is a day-of-week index (0-6), or a Date object
		function isHiddenDay(day) {
			if (typeof day == 'object') {
				day = day.getDay();
			}
			return isHiddenDayHash[day];
		}


		function getCellsPerWeek() {
			return cellsPerWeek;
		}


		// Keep incrementing the current day until it is no longer a hidden day.
		// If the initial value of `date` is not a hidden day, don't do anything.
		// Pass `isExclusive` as `true` if you are dealing with an end date.
		// `inc` defaults to `1` (increment one day forward each time)
		function skipHiddenDays(date, inc, isExclusive) {
			inc = inc || 1;
			while (
				isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
			) {
				addDays(date, inc);
			}
		}


		//
		// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
		//

		// cell -> date (combines all transformations)
		// Possible arguments:
		// - row, col
		// - { row:#, col: # }
		function cellToDate() {
			var cellOffset = cellToCellOffset.apply(null, arguments);
			var dayOffset = cellOffsetToDayOffset(cellOffset);
			var date = dayOffsetToDate(dayOffset);
			return date;
		}

		// cell -> cell offset
		// Possible arguments:
		// - row, col
		// - { row:#, col:# }
		function cellToCellOffset(row, col) {
			var colCnt = t.getColCnt();

			// rtl variables. wish we could pre-populate these. but where?
			var dis = isRTL ? -1 : 1;
			var dit = isRTL ? colCnt - 1 : 0;

			if (typeof row == 'object') {
				col = row.col;
				row = row.row;
			}
			var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)

			return cellOffset;
		}

		// cell offset -> day offset
		function cellOffsetToDayOffset(cellOffset) {
			var day0 = t.visStart.getDay(); // first date's day of week
			cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
			return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
				+ cellToDayMap[ // # of days from partial last week
					(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
				]
				- day0; // adjustment for beginning-of-week normalization
		}

		// day offset -> date (JavaScript Date object)
		function dayOffsetToDate(dayOffset) {
			var date = cloneDate(t.visStart);
			addDays(date, dayOffset);
			return date;
		}


		//
		// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
		//

		// date -> cell (combines all transformations)
		function dateToCell(date) {
			var dayOffset = dateToDayOffset(date);
			var cellOffset = dayOffsetToCellOffset(dayOffset);
			var cell = cellOffsetToCell(cellOffset);
			return cell;
		}

		// date -> day offset
		function dateToDayOffset(date) {
			return dayDiff(date, t.visStart);
		}

		// day offset -> cell offset
		function dayOffsetToCellOffset(dayOffset) {
			var day0 = t.visStart.getDay(); // first date's day of week
			dayOffset += day0; // normalize dayOffset to beginning-of-week
			return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
				+ dayToCellMap[ // # of cells from partial last week
					(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
				]
				- dayToCellMap[day0]; // adjustment for beginning-of-week normalization
		}

		// cell offset -> cell (object with row & col keys)
		function cellOffsetToCell(cellOffset) {
			var colCnt = t.getColCnt();

			// rtl variables. wish we could pre-populate these. but where?
			var dis = isRTL ? -1 : 1;
			var dit = isRTL ? colCnt - 1 : 0;

			var row = Math.floor(cellOffset / colCnt);
			var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
			return {
				row: row,
				col: col
			};
		}

		//
		// Converts a date range into an array of segment objects.
		// "Segments" are horizontal stretches of time, sliced up by row.
		// A segment object has the following properties:
		// - row
		// - cols
		// - isStart
		// - isEnd
		//
		function rangeToSegments(startDate, endDate, ev) {
			var rowCnt = t.getRowCnt();
			var colCnt = t.getColCnt();
			var segments = []; // array of segments to return
			// day offset for given date range
			var rangeDayOffsetStart = dateToDayOffset(startDate);
			var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive

			//RHC START
			//2.01 already supports nextDayThreshold, but we are not moving yet to 2.01
			// the original supports minutes
			nextDayThreshold  = options.nextDayThreshold && ''!=options.nextDayThreshold ? parseInt(options.nextDayThreshold) : false;
			if( ev && ev.end && ev.end.getHours && false!==nextDayThreshold && (rangeDayOffsetEnd-rangeDayOffsetStart)>1 ){		
				if( ev.end.getHours() <= nextDayThreshold ){
					rangeDayOffsetEnd = rangeDayOffsetEnd - 1 ;
				}	
			}
			//RHC END
			
			// first and last cell offset for the given date range
			// "last" implies inclusivity
			var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
			var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;

			// loop through all the rows in the view
			for (var row=0; row<rowCnt; row++) {

				// first and last cell offset for the row
				var rowCellOffsetFirst = row * colCnt;
				var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;

				// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
				var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
				var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);

				// make sure segment's offsets are valid and in view
				if (segmentCellOffsetFirst <= segmentCellOffsetLast) {

					// translate to cells
					var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
					var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);

					// view might be RTL, so order by leftmost column
					var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();

					// Determine if segment's first/last cell is the beginning/end of the date range.
					// We need to compare "day offset" because "cell offsets" are often ambiguous and
					// can translate to multiple days, and an edge case reveals itself when we the
					// range's first cell is hidden (we don't want isStart to be true).
					var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
					var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively

					segments.push({
						row: row,
						leftCol: cols[0],
						rightCol: cols[1],
						isStart: isStart,
						isEnd: isEnd
					});
				}
			}
			/* RHC START */
			if( $(t.element).parents('.rhcalendar.not-widget').hasClass('fc-small') ){
				new_segments = [];
				$.each(segments,function(i,row){
					leftCol = row.leftCol;
					rightCol = row.rightCol;
					if(leftCol<rightCol){
						for(a=leftCol; a<=rightCol; a++){
							var new_row = jQuery.extend({}, row);
							new_row.leftCol = a;
							new_row.rightCol = a;				
							new_segments.push(new_row);
						}
					}else{
						new_segments.push(row);
					}
				});
				return new_segments;
			}
			/* RHC END */   
			return segments;
		}
		

	}

	function OverlayManager() {
		var t = this;
		
		
		// exports
		t.renderOverlay = renderOverlay;
		t.clearOverlays = clearOverlays;
		
		
		// locals
		var usedOverlays = [];
		var unusedOverlays = [];
		
		
		function renderOverlay(rect, parent) {
			var e = unusedOverlays.shift();
			if (!e) {
				e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
			}
			if (e[0].parentNode != parent[0]) {
				e.appendTo(parent);
			}
			usedOverlays.push(e.css(rect).show());
			return e;
		}
		

		function clearOverlays() {
			var e;
			while (e = usedOverlays.shift()) {
				unusedOverlays.push(e.hide().unbind());
			}
		}


	}

	function SelectionManager() {
		var t = this;
		
		
		// exports
		t.select = select;
		t.unselect = unselect;
		t.reportSelection = reportSelection;
		t.daySelectionMousedown = daySelectionMousedown;
		
		
		// imports
		var opt = t.opt;
		var trigger = t.trigger;
		var defaultSelectionEnd = t.defaultSelectionEnd;
		var renderSelection = t.renderSelection;
		var clearSelection = t.clearSelection;
		
		
		// locals
		var selected = false;



		// unselectAuto
		if (opt('selectable') && opt('unselectAuto')) {
			$(document).mousedown(function(ev) {
				var ignore = opt('unselectCancel');
				if (ignore) {
					if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
						return;
					}
				}
				unselect(ev);
			});
		}
		

		function select(startDate, endDate, allDay) {
			unselect();
			if (!endDate) {
				endDate = defaultSelectionEnd(startDate, allDay);
			}
			renderSelection(startDate, endDate, allDay);
			reportSelection(startDate, endDate, allDay);
		}
		
		
		function unselect(ev) {
			if (selected) {
				selected = false;
				clearSelection();
				trigger('unselect', null, ev);
			}
		}
		
		
		function reportSelection(startDate, endDate, allDay, ev) {
			selected = true;
			trigger('select', null, startDate, endDate, allDay, ev);
		}
		
		
		function daySelectionMousedown(ev) { // not really a generic manager method, oh well
			var cellToDate = t.cellToDate;
			var getIsCellAllDay = t.getIsCellAllDay;
			var hoverListener = t.getHoverListener();
			var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
			if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
				unselect(ev);
				var _mousedownElement = this;
				var dates;
				hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
					clearSelection();
					if (cell && getIsCellAllDay(cell)) {
						dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
						renderSelection(dates[0], dates[1], true);
					}else{
						dates = null;
					}
				}, ev);
				$(document).one('mouseup', function(ev) {
					hoverListener.stop();
					if (dates) {
						if (+dates[0] == +dates[1]) {
							reportDayClick(dates[0], true, ev);
						}
						reportSelection(dates[0], dates[1], true, ev);
					}
				});
			}
		}


	}

	function BasicEventRenderer() {
		var t = this;
		
		
		// exports
		t.renderEvents = renderEvents;
		t.clearEvents = clearEvents;
		

		// imports
		DayEventRenderer.call(t);

		
		function renderEvents(events, modifiedEventId) {
			t.renderDayEvents(events, modifiedEventId);
		}
		
		
		function clearEvents() {
			t.getDaySegmentContainer().empty();
		}


		// TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div

	}

	function DayEventRenderer() {
		var t = this;

		
		// exports
		t.renderDayEvents = renderDayEvents;
		t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
		t.resizableDayEvent = resizableDayEvent; // "
		
		
		// imports
		var opt = t.opt;
		var trigger = t.trigger;
		var isEventDraggable = t.isEventDraggable;
		var isEventResizable = t.isEventResizable;
		var eventEnd = t.eventEnd;
		var reportEventElement = t.reportEventElement;
		var eventElementHandlers = t.eventElementHandlers;
		var showEvents = t.showEvents;
		var hideEvents = t.hideEvents;
		var eventDrop = t.eventDrop;
		var eventResize = t.eventResize;
		var getRowCnt = t.getRowCnt;
		var getColCnt = t.getColCnt;
		var getColWidth = t.getColWidth;
		var allDayRow = t.allDayRow; // TODO: rename
		var colLeft = t.colLeft;
		var colRight = t.colRight;
		var colContentLeft = t.colContentLeft;
		var colContentRight = t.colContentRight;
		var dateToCell = t.dateToCell;
		var getDaySegmentContainer = t.getDaySegmentContainer;
		var formatDates = t.calendar.formatDates;
		var renderDayOverlay = t.renderDayOverlay;
		var clearOverlays = t.clearOverlays;
		var clearSelection = t.clearSelection;
		var getHoverListener = t.getHoverListener;
		var rangeToSegments = t.rangeToSegments;
		var cellToDate = t.cellToDate;
		var cellToCellOffset = t.cellToCellOffset;
		var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
		var dateToDayOffset = t.dateToDayOffset;
		var dayOffsetToCellOffset = t.dayOffsetToCellOffset;


		// Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
		// Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
		// Can only be called when the event container is empty (because it wipes out all innerHTML).
		function renderDayEvents(events, modifiedEventId) {

			// do the actual rendering. Receive the intermediate "segment" data structures.
			var segments = _renderDayEvents(
				events,
				false, // don't append event elements
				true // set the heights of the rows
			);

			// report the elements to the View, for general drag/resize utilities
			segmentElementEach(segments, function(segment, element) {
				reportEventElement(segment.event, element);
			});

			// attach mouse handlers
			attachHandlers(segments, modifiedEventId);

			// call `eventAfterRender` callback for each event
			segmentElementEach(segments, function(segment, element) {
				trigger('eventAfterRender', segment.event, segment.event, element);
			});
		}


		// Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
		// Append this event element to the event container, which might already be populated with events.
		// If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
		// This hack is used to maintain continuity when user is manually resizing an event.
		// Returns an array of DOM elements for the event.
		function renderTempDayEvent(event, adjustRow, adjustTop) {

			// actually render the event. `true` for appending element to container.
			// Recieve the intermediate "segment" data structures.
			var segments = _renderDayEvents(
				[ event ],
				true, // append event elements
				false // don't set the heights of the rows
			);

			var elements = [];

			// Adjust certain elements' top coordinates
			segmentElementEach(segments, function(segment, element) {
				if (segment.row === adjustRow) {
					element.css('top', adjustTop);
				}
				elements.push(element[0]); // accumulate DOM nodes
			});

			return elements;
		}


		// Render events onto the calendar. Only responsible for the VISUAL aspect.
		// Not responsible for attaching handlers or calling callbacks.
		// Set `doAppend` to `true` for rendering elements without clearing the existing container.
		// Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
		function _renderDayEvents(events, doAppend, doRowHeights) {

			// where the DOM nodes will eventually end up
			var finalContainer = getDaySegmentContainer();

			// the container where the initial HTML will be rendered.
			// If `doAppend`==true, uses a temporary container.
			var renderContainer = doAppend ? $("<div/>") : finalContainer;

			var segments = buildSegments(events);
			var html;
			var elements;

			// calculate the desired `left` and `width` properties on each segment object
			calculateHorizontals(segments);

			// build the HTML string. relies on `left` property
			html = buildHTML(segments);

			// render the HTML. innerHTML is considerably faster than jQuery's .html()
			renderContainer[0].innerHTML = html;

			// retrieve the individual elements
			elements = renderContainer.children();

			// if we were appending, and thus using a temporary container,
			// re-attach elements to the real container.
			if (doAppend) {
				finalContainer.append(elements);
			}

			// assigns each element to `segment.event`, after filtering them through user callbacks
			resolveElements(segments, elements);

			// Calculate the left and right padding+margin for each element.
			// We need this for setting each element's desired outer width, because of the W3C box model.
			// It's important we do this in a separate pass from acually setting the width on the DOM elements
			// because alternating reading/writing dimensions causes reflow for every iteration.
			segmentElementEach(segments, function(segment, element) {
				segment.hsides = hsides(element, true); // include margins = `true`
			});

			// Set the width of each element
			segmentElementEach(segments, function(segment, element) {
				element.width(
					Math.max(0, segment.outerWidth - segment.hsides)
				);
			});

			// Grab each element's outerHeight (setVerticals uses this).
			// To get an accurate reading, it's important to have each element's width explicitly set already.
			segmentElementEach(segments, function(segment, element) {
				segment.outerHeight = element.outerHeight(true); // include margins = `true`
			});

			// Set the top coordinate on each element (requires segment.outerHeight)
			setVerticals(segments, doRowHeights);

			return segments;
		}


		// Generate an array of "segments" for all events.
		function buildSegments(events) {
			var segments = [];
			for (var i=0; i<events.length; i++) {
				var eventSegments = buildSegmentsForEvent(events[i]);
				segments.push.apply(segments, eventSegments); // append an array to an array
			}
			return segments;
		}


		// Generate an array of segments for a single event.
		// A "segment" is the same data structure that View.rangeToSegments produces,
		// with the addition of the `event` property being set to reference the original event.
		function buildSegmentsForEvent(event) {
			var startDate = event.start;
			var endDate = exclEndDay(event);
			var segments = rangeToSegments(startDate, endDate, event);
			for (var i=0; i<segments.length; i++) {
				segments[i].event = event;
			}

			return segments;
		}


		// Sets the `left` and `outerWidth` property of each segment.
		// These values are the desired dimensions for the eventual DOM elements.
		function calculateHorizontals(segments) {
			var isRTL = opt('isRTL');
			for (var i=0; i<segments.length; i++) {
				var segment = segments[i];

				// Determine functions used for calulating the elements left/right coordinates,
				// depending on whether the view is RTL or not.
				// NOTE:
				// colLeft/colRight returns the coordinate butting up the edge of the cell.
				// colContentLeft/colContentRight is indented a little bit from the edge.
				var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
				var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;

				var left = leftFunc(segment.leftCol);
				var right = rightFunc(segment.rightCol);
				segment.left = left;
				segment.outerWidth = right - left;
			}
		}


		// Build a concatenated HTML string for an array of segments
		function buildHTML(segments) {
			var html = '';
			for (var i=0; i<segments.length; i++) {
				html += buildHTMLForSegment(segments[i]);
			}
			return html;
		}


		// Build an HTML string for a single segment.
		// Relies on the following properties:
		// - `segment.event` (from `buildSegmentsForEvent`)
		// - `segment.left` (from `calculateHorizontals`)
		function buildHTMLForSegment(segment) {
			var html = '';
			var isRTL = opt('isRTL');
			var event = segment.event;
			var url = event.url;

			// generate the list of CSS classNames
			var classNames = [ 'fc-event', 'fc-event-hori' ];
			if (isEventDraggable(event)) {
				classNames.push('fc-event-draggable');
			}
			if (segment.isStart) {
				classNames.push('fc-event-start');
			}
			if (segment.isEnd) {
				classNames.push('fc-event-end');
			}
			// use the event's configured classNames
			// guaranteed to be an array via `normalizeEvent`
			classNames = classNames.concat(event.className);
			if (event.source) {
				// use the event's source's classNames, if specified
				classNames = classNames.concat(event.source.className || []);
			}

			// generate a semicolon delimited CSS string for any of the "skin" properties
			// of the event object (`backgroundColor`, `borderColor` and such)
			var skinCss = getSkinCss(event, opt);

			if (url) {
				html += "<a href='" + htmlEscape(url) + "'";
			}else{
				html += "<div";
			}
			html +=
				" class='" + classNames.join(' ') + "'" +
				" style=" +
					"'" +
					"position:absolute;" +
					"left:" + segment.left + "px;" +
					skinCss +
					"'" +
				">" +
				"<div class='fc-event-inner'>";
			if (!event.allDay && segment.isStart) {
				html +=
					"<span class='fc-event-time'>" +
					htmlEscape(
						formatDates(event.start, event.end, opt('timeFormat'))
					) +
					"</span>";
			}
			html +=
				"<span class='fc-event-title'>" +
				htmlEscape(event.title || '') +
				"</span>" +
				"</div>";
			if (segment.isEnd && isEventResizable(event)) {
				html +=
					"<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
					"&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
					"</div>";
			}
			html += "</" + (url ? "a" : "div") + ">";

			// TODO:
			// When these elements are initially rendered, they will be briefly visibile on the screen,
			// even though their widths/heights are not set.
			// SOLUTION: initially set them as visibility:hidden ?

			return html;
		}


		// Associate each segment (an object) with an element (a jQuery object),
		// by setting each `segment.element`.
		// Run each element through the `eventRender` filter, which allows developers to
		// modify an existing element, supply a new one, or cancel rendering.
		function resolveElements(segments, elements) {
			for (var i=0; i<segments.length; i++) {
				var segment = segments[i];
				var event = segment.event;
				var element = elements.eq(i);

				// call the trigger with the original element
				var triggerRes = trigger('eventRender', event, event, element);

				if (triggerRes === false) {
					// if `false`, remove the event from the DOM and don't assign it to `segment.event`
					element.remove();
				}
				else {
					if (triggerRes && triggerRes !== true) {
						// the trigger returned a new element, but not `true` (which means keep the existing element)

						// re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
						triggerRes = $(triggerRes)
							.css({
								position: 'absolute',
								left: segment.left
							});

						element.replaceWith(triggerRes);
						element = triggerRes;
					}

					segment.element = element;
				}
			}
		}



		/* Top-coordinate Methods
		-------------------------------------------------------------------------------------------------*/


		// Sets the "top" CSS property for each element.
		// If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
		// so that if elements vertically overflow, the cell expands vertically to compensate.
		function setVerticals(segments, doRowHeights) {
			var rowContentHeights = calculateVerticals(segments); // also sets segment.top
			var rowContentElements = getRowContentElements(); // returns 1 inner div per row
			var rowContentTops = [];
			var rowCnt = getRowCnt();
			var colCnt = getColCnt();
			var holderHeights = [];
			var holderEvents = [];

			var calendar = t.calendar;
			var formatDate = calendar.formatDate;

			for(row=0;row<rowCnt;row++){
				holderHeights[ row ] = [];
				holderEvents[ row ] = [];

				segmentElementEach(segments, function(segment, element) {
		  			if ( segment.row != row ) return;

					if ( segment.leftCol > segment.rightcol ) return;

					for(col=segment.leftCol; col<=segment.rightCol; col++){
						holderHeights[ row ][ col ] = holderHeights[ row ][ col ] || 0;
						holderHeights[ row ][ col ] += segment.outerHeight;
						
						if ( 'undefined' === typeof holderEvents[ row ][ col ] ) {
							holderEvents[ row ][ col ] = [];
						}

						holderEvents[ row ][ col ].push( segment.event );
					}
				});
			}

			//set the content of each cell just enough tall to hold overlaid events. (fluid design)
			if (doRowHeights) {
				for (var i=0; i<rowContentElements.length; i++) {
					if( rowContentElements[i].length >= holderHeights[i].length ){
						for (var j=0; j<rowContentElements[i].length; j++){
							h = holderHeights[i] && holderHeights[i][j] ? holderHeights[i][j] : false;

							if ( false === h ) {
								rowContentElements[i].eq(j).addClass('fc-day-no-events');
							} else {
								var events = holderEvents[i][j];
								var $fcDay = rowContentElements[i].eq(j).closest('.fc-day');
								var $fcDayNumber = $fcDay.find('.fc-day-number');
								var linksTarget = '1' === calendar.options.rhc_year.links_in_new_tab ? 'target="_blank"' : '';
								var eventsHtml = '';

								$fcDay.addClass('fc-have-event');
								
								if ('overlay' === calendar.options.rhc_year.click_action) {
									$.each(events, function(i, event) {
										var time;
										var taxonomies = [];
										var terms = [];
										var title;
										var termsStartFrom = 2;
										
										if ( event.allDay ) {
											time = calendar.options.rhc_year.overlay_allday_label;
										} else {
											time = formatDate(event._start, calendar.options.rhc_year.overlay_time_format)+' - ';
											time += formatDate(event._end, calendar.options.rhc_year.overlay_time_format)
										}

										eventsHtml += '<div class="fc-day-content-event">';
										eventsHtml += '<div class="fc-day-content-event-time">'+time+'</div>';

										if ('1' === calendar.options.rhc_year.overlay_event_link) {
											title = '<a href="'+event.url+'"'+linksTarget+'>'+event.title+'</a>';
										} else {
											title = event.title;
										}

										eventsHtml += '<div class="fc-day-content-event-title">'+title+'</div>';

										if ('0' === calendar.options.rhc_year.disable_calendar && '1' === calendar.options.rhc_year.overlay_calendar_taxonomy) {
											taxonomies.push('calendar');
										}

										if ('0' === calendar.options.rhc_year.disable_organizer && '1' === calendar.options.rhc_year.overlay_organizer_taxonomy) {
											taxonomies.push('organizer');
										}

										if ('0' === calendar.options.rhc_year.disable_venue && '1' === calendar.options.rhc_year.overlay_venue_taxonomy) {
											taxonomies.push('venue');
										}

										if ('0' === calendar.options.rhc_year.adm_lst_disabled_tax_state && '1' === calendar.options.rhc_year.overlay_state_taxonomy) {
											taxonomies.push('state');
										}

										if ('0' === calendar.options.rhc_year.adm_lst_disabled_tax_country && '1' === calendar.options.rhc_year.overlay_country_taxonomy) {
											taxonomies.push('country');
										}

										if ('0' === calendar.options.rhc_year.adm_lst_disabled_tax_city && '1' === calendar.options.rhc_year.overlay_city_taxonomy) {
											taxonomies.push('city');
										}

										if (event.terms.length > termsStartFrom) {
											for (var x = termsStartFrom; x < event.terms.length; x++) {
												if ('undefined' === typeof terms[event.terms[x].taxonomy]) {
													terms[event.terms[x].taxonomy] = [];
												}

												if ('1' === calendar.options.rhc_year.overlay_taxonomy_links) {
													terms[event.terms[x].taxonomy].push('<a href="'+event.terms[x].url+'"'+linksTarget+'>'+event.terms[x].name+'</a>');
												} else {
													terms[event.terms[x].taxonomy].push(event.terms[x].name);
												}
											}
										}
										
										if (taxonomies) {
											var taxonomyIcon = '1' === calendar.options.rhc_year.overlay_taxonomy_icons ? 'fc-day-content-event-taxonomy-icon ' : '';
											
											$.each(taxonomies, function(i, taxonomy) {
												if ( 'undefined' !== typeof terms[taxonomy] ) {
													eventsHtml += '<div class="fc-day-content-event-taxonomy '+taxonomyIcon+'fc-day-content-event-'+taxonomy+'">'+terms[taxonomy].join(', ')+'</div>';
												}
											});
										}

										eventsHtml += '</div>';
									});

									if ( eventsHtml.length > 0 ) {
										$fcDay.find('.fc-day-content-events').html(eventsHtml);
									}

									$fcDayNumber
										.on('click', function() {
											var fcDayContent = $(this).closest('.fc-day').find('.fc-day-content-overlay').html();
											
											$('.eyv-overlay')
												.find('.eyv-overlay-content')
												.html(fcDayContent)
												.wrapInner('<div></div>')
												.end()
												.addClass('open')
												.removeClass('close')
											;
										})
									;
								} else {
									var date = new Date(rowContentElements[i][j].textContent);
									var dayLinkUrl = '<a href="?defaultview='+calendar.options.rhc_year.click_action;
									
									dayLinkUrl += '&gotodate='+date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate();
									
									if ('rhc_event' === calendar.options.rhc_year.click_action) {
										dayLinkUrl += '&eventlistupcoming=0';
									}

									dayLinkUrl += '"'+linksTarget+'>'+$fcDayNumber.text()+'</a>';
									$fcDayNumber.html(dayLinkUrl);
								}
							}
						}
					}else{
						//this applies to agenda views
						new_h = 0;
						$.each(holderHeights[i],function(k,h){
							new_h = h>new_h?h:new_h;
						});
						rowContentElements[i].height( new_h );
					}  
				}
			}		
	
			for (var i=0; i<rowContentElements.length; i++) {
				for (var j=0; j<rowContentElements[i].length; j++){	
					rowContentTops[i] = rowContentTops[i]||[];
					rowContentTops[i][j] = rowContentElements[i].eq(j).position().top;
				}
			}

			// Set each segment element's CSS "top" property.
			// Each segment object has a "top" property, which is relative to the row's top, but...		
			segmentElementEach(segments, function(segment, element) {
				contentTop = rowContentTops[segment.row][segment.leftCol] || rowContentTops[segment.row][0] ; 
				element.css(
					'top',
					contentTop + segment.top // ...now, relative to views's origin
				);
			});
		}


		// Calculate the "top" coordinate for each segment, relative to the "top" of the row.
		// Also, return an array that contains the "content" height for each row
		// (the height displaced by the vertically stacked events in the row).
		// Requires segments to have their `outerHeight` property already set.
		function calculateVerticals(segments) {
			var rowCnt = getRowCnt();
			var colCnt = getColCnt();
			var rowContentHeights = []; // content height for each row
			var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row

			for (var rowI=0; rowI<rowCnt; rowI++) {
				var segmentRow = segmentRows[rowI];

				// an array of running total heights for each column.
				// initialize with all zeros.
				var colHeights = [];
				for (var colI=0; colI<colCnt; colI++) {
					colHeights.push(0);
				}

				// loop through every segment
				for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
					var segment = segmentRow[segmentI];

					// find the segment's top coordinate by looking at the max height
					// of all the columns the segment will be in.
					segment.top = arrayMax(
						colHeights.slice(
							segment.leftCol,
							segment.rightCol + 1 // make exclusive for slice
						)
					);

					// adjust the columns to account for the segment's height
					for (var colI=segment.leftCol; colI<=segment.rightCol; colI++) {
						colHeights[colI] = segment.top + segment.outerHeight;
					}
				}

				// the tallest column in the row should be the "content height"
				rowContentHeights.push(arrayMax(colHeights));
			}

			return rowContentHeights;
		}


		// Build an array of segment arrays, each representing the segments that will
		// be in a row of the grid, sorted by which event should be closest to the top.
		function buildSegmentRows(segments) {
			var rowCnt = getRowCnt();
			var segmentRows = [];
			var segmentI;
			var segment;
			var rowI;

			// group segments by row
			for (segmentI=0; segmentI<segments.length; segmentI++) {
				segment = segments[segmentI];
				rowI = segment.row;
				if (segment.element) { // was rendered?
					if (segmentRows[rowI]) {
						// already other segments. append to array
						segmentRows[rowI].push(segment);
					}
					else {
						// first segment in row. create new array
						segmentRows[rowI] = [ segment ];
					}
				}
			}

			// sort each row
			for (rowI=0; rowI<rowCnt; rowI++) {
				segmentRows[rowI] = sortSegmentRow(
					segmentRows[rowI] || [] // guarantee an array, even if no segments
				);
			}

			return segmentRows;
		}


		// Sort an array of segments according to which segment should appear closest to the top
		function sortSegmentRow(segments) {
			var sortedSegments = [];

			// build the subrow array
			var subrows = buildSegmentSubrows(segments);

			// flatten it
			for (var i=0; i<subrows.length; i++) {
				sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
			}

			return sortedSegments;
		}


		// Take an array of segments, which are all assumed to be in the same row,
		// and sort into subrows.
		function buildSegmentSubrows(segments) {

			// Give preference to elements with certain criteria, so they have
			// a chance to be closer to the top.
			segments.sort(compareDaySegments);

			var subrows = [];
			for (var i=0; i<segments.length; i++) {
				var segment = segments[i];

				// loop through subrows, starting with the topmost, until the segment
				// doesn't collide with other segments.
				for (var j=0; j<subrows.length; j++) {
					if (!isDaySegmentCollision(segment, subrows[j])) {
						break;
					}
				}
				// `j` now holds the desired subrow index
				if (subrows[j]) {
					subrows[j].push(segment);
				}
				else {
					subrows[j] = [ segment ];
				}
			}

			return subrows;
		}


		// Return an array of jQuery objects for the placeholder content containers of each row.
		// The content containers don't actually contain anything, but their dimensions should match
		// the events that are overlaid on top.
		function getRowContentElements() {
			var i;
			var rowCnt = getRowCnt();
			var rowDivs = [];
			for (i=0; i<rowCnt; i++) {
				rowDivs[i] = allDayRow(i)
					.find('div.fc-day-content > div.fc-day-content-date');
			}
			return rowDivs;
		}



		/* Mouse Handlers
		---------------------------------------------------------------------------------------------------*/
		// TODO: better documentation!


		function attachHandlers(segments, modifiedEventId) {
			var segmentContainer = getDaySegmentContainer();

			segmentElementEach(segments, function(segment, element, i) {
				var event = segment.event;
				if (event._id === modifiedEventId) {
					bindDaySeg(event, element, segment);
				}else{
					element[0]._fci = i; // for lazySegBind
				}
			});

			lazySegBind(segmentContainer, segments, bindDaySeg);
		}


		function bindDaySeg(event, eventElement, segment) {

			if (isEventDraggable(event)) {
				t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
			}

			if (
				segment.isEnd && // only allow resizing on the final segment for an event
				isEventResizable(event)
			) {
				t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
			}

			// attach all other handlers.
			// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
			eventElementHandlers(event, eventElement);
		}

		
		function draggableDayEvent(event, eventElement) {
			var hoverListener = getHoverListener();
			var dayDelta;
			eventElement.draggable({
				delay: 50,
				opacity: opt('dragOpacity'),
				revertDuration: opt('dragRevertDuration'),
				start: function(ev, ui) {
					trigger('eventDragStart', eventElement, event, ev, ui);
					hideEvents(event, eventElement);
					hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
						eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
						clearOverlays();
						if (cell) {
							var origDate = cellToDate(origCell);
							var date = cellToDate(cell);
							dayDelta = dayDiff(date, origDate);
							renderDayOverlay(
								addDays(cloneDate(event.start), dayDelta),
								addDays(exclEndDay(event), dayDelta)
							);
						}else{
							dayDelta = 0;
						}
					}, ev, 'drag');
				},
				stop: function(ev, ui) {
					hoverListener.stop();
					clearOverlays();
					trigger('eventDragStop', eventElement, event, ev, ui);
					if (dayDelta) {
						eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
					}else{
						eventElement.css('filter', ''); // clear IE opacity side-effects
						showEvents(event, eventElement);
					}
				}
			});
		}

		
		function resizableDayEvent(event, element, segment) {
			var isRTL = opt('isRTL');
			var direction = isRTL ? 'w' : 'e';
			var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
			var isResizing = false;
			
			// TODO: look into using jquery-ui mouse widget for this stuff
			disableTextSelection(element); // prevent native <a> selection for IE
			element
				.mousedown(function(ev) { // prevent native <a> selection for others
					ev.preventDefault();
				})
				.click(function(ev) {
					if (isResizing) {
						ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
						ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
						                               // (eventElementHandlers needs to be bound after resizableDayEvent)
					}
				});
			
			handle.mousedown(function(ev) {
				if (ev.which != 1) {
					return; // needs to be left mouse button
				}
				isResizing = true;
				var hoverListener = getHoverListener();
				var rowCnt = getRowCnt();
				var colCnt = getColCnt();
				var elementTop = element.css('top');
				var dayDelta;
				var helpers;
				var eventCopy = $.extend({}, event);
				var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) );
				clearSelection();
				$('body')
					.css('cursor', direction + '-resize')
					.one('mouseup', mouseup);
				trigger('eventResizeStart', this, event, ev);
				hoverListener.start(function(cell, origCell) {
					if (cell) {

						var origCellOffset = cellToCellOffset(origCell);
						var cellOffset = cellToCellOffset(cell);

						// don't let resizing move earlier than start date cell
						cellOffset = Math.max(cellOffset, minCellOffset);

						dayDelta =
							cellOffsetToDayOffset(cellOffset) -
							cellOffsetToDayOffset(origCellOffset);

						if (dayDelta) {
							eventCopy.end = addDays(eventEnd(event), dayDelta, true);
							var oldHelpers = helpers;

							helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
							helpers = $(helpers); // turn array into a jQuery object

							helpers.find('*').css('cursor', direction + '-resize');
							if (oldHelpers) {
								oldHelpers.remove();
							}

							hideEvents(event);
						}
						else {
							if (helpers) {
								showEvents(event);
								helpers.remove();
								helpers = null;
							}
						}
						clearOverlays();
						renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
							event.start,
							addDays( exclEndDay(event), dayDelta )
							// TODO: instead of calling renderDayOverlay() with dates,
							// call _renderDayOverlay (or whatever) with cell offsets.
						);
					}
				}, ev);
				
				function mouseup(ev) {
					trigger('eventResizeStop', this, event, ev);
					$('body').css('cursor', '');
					hoverListener.stop();
					clearOverlays();
					if (dayDelta) {
						eventResize(this, event, dayDelta, 0, ev);
						// event redraw will clear helpers
					}
					// otherwise, the drag handler already restored the old events
					
					setTimeout(function() { // make this happen after the element's click event
						isResizing = false;
					},0);
				}
			});
		}
		

	}

	function disableTextSelection(element) {
		element
			.attr('unselectable', 'on')
			.css('MozUserSelect', 'none')
			.bind('selectstart.ui', function() { return false; });
	}

	function HorizontalPositionCache(getElement) {

		var t = this,
			elements = {},
			lefts = {},
			rights = {};
			
		function e(i) {
			return elements[i] = elements[i] || getElement(i);
		}
		
		t.left = function(i) {
			return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
		};
		
		t.right = function(i) {
			return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
		};
		
		t.clear = function() {
			elements = {};
			lefts = {};
			rights = {};
		};
		
	}

	function cloneDate(d, dontKeepTime) {
		if (dontKeepTime) {
			return clearTime(new Date(+d));
		}
		return new Date(+d);
	}

	function clearTime(d) {
		d.setHours(0);
		d.setMinutes(0);
		d.setSeconds(0); 
		d.setMilliseconds(0);
		return d;
	}

	function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
		if (obj[name] !== undefined) {
			return obj[name];
		}
		var parts = name.split(/(?=[A-Z])/),
			i=parts.length-1, res;
		for (; i>=0; i--) {
			res = obj[parts[i].toLowerCase()];
			if (res !== undefined) {
				return res;
			}
		}
		return obj[''];
	}

	function addDays(d, n, keepTime) { // deals with daylight savings
		if (+d) {
			var dd = d.getDate() + n,
				check = cloneDate(d);
			check.setHours(9); // set to middle of day
			check.setDate(dd);
			d.setDate(dd);
			if (!keepTime) {
				clearTime(d);
			}
			fixDate(d, check);
		}
		return d;
	}

	function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
		if (+d) { // prevent infinite looping on invalid dates
			while (d.getDate() != check.getDate()) {
				d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
			}
		}
	}

	function htmlEscape(s) {
		return s.replace(/&/g, '&amp;')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;')
			.replace(/'/g, '&#039;')
			.replace(/"/g, '&quot;')
			.replace(/\n/g, '<br />');
	}

	var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
		DAY_MS = 86400000,
		HOUR_MS = 3600000,
		MINUTE_MS = 60000;

	function markFirstLast(e) {
		e.children()
			.removeClass('fc-first fc-last')
			.filter(':first-child')
				.addClass('fc-first')
			.end()
			.filter(':last-child')
				.addClass('fc-last');
	}

	function CoordinateGrid(buildFunc) {

		var t = this;
		var rows;
		var cols;
		
		
		t.build = function() {
			rows = [];
			cols = [];
			buildFunc(rows, cols);
		};
		
		
		t.cell = function(x, y) {
			var rowCnt = rows.length;
			var colCnt = cols.length;
			var i, r=-1, c=-1;
			for (i=0; i<rowCnt; i++) {
				if (y >= rows[i][0] && y < rows[i][1]) {
					r = i;
					break;
				}
			}
			for (i=0; i<colCnt; i++) {
				if (x >= cols[i][0] && x < cols[i][1]) {
					c = i;
					break;
				}
			}
			return (r>=0 && c>=0) ? { row:r, col:c } : null;
		};
		
		
		t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
			var origin = originElement.offset();
			return {
				top: rows[row0][0] - origin.top,
				left: cols[col0][0] - origin.left,
				width: cols[col1][1] - cols[col0][0],
				height: rows[row1][1] - rows[row0][0]
			};
		};

	}

	function HoverListener(coordinateGrid) {


		var t = this;
		var bindType;
		var change;
		var firstCell;
		var cell;
		
		
		t.start = function(_change, ev, _bindType) {
			change = _change;
			firstCell = cell = null;
			coordinateGrid.build();
			mouse(ev);
			bindType = _bindType || 'mousemove';
			$(document).bind(bindType, mouse);
		};
		
		
		function mouse(ev) {
			_fixUIEvent(ev); // see below
			var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
			if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
				if (newCell) {
					if (!firstCell) {
						firstCell = newCell;
					}
					change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
				}else{
					change(newCell, firstCell);
				}
				cell = newCell;
			}
		}
		
		
		t.stop = function() {
			$(document).unbind(bindType, mouse);
			return cell;
		};
		
		
	}

	function exclEndDay(event) {
		if (event.end) {
			return _exclEndDay(event.end, event.allDay);
		}else{
			return addDays(cloneDate(event.start), 1);
		}
	}

	function _exclEndDay(end, allDay) {
		end = cloneDate(end);
		return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
		// why don't we check for seconds/ms too?
	}

	function dayDiff(d1, d2) { // d1 - d2
		return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
	}

	function firstDefined() {
		for (var i=0; i<arguments.length; i++) {
			if (arguments[i] !== undefined) {
				return arguments[i];
			}
		}
	}

	function getSkinCss(event, opt) {
		var source = event.source || {};
		var eventColor = event.color;
		var sourceColor = source.color;
		var optionColor = opt('eventColor');
		var backgroundColor =
			event.backgroundColor ||
			eventColor ||
			source.backgroundColor ||
			sourceColor ||
			opt('eventBackgroundColor') ||
			optionColor;
		var borderColor =
			event.borderColor ||
			eventColor ||
			source.borderColor ||
			sourceColor ||
			opt('eventBorderColor') ||
			optionColor;
		var textColor =
			event.textColor ||
			source.textColor ||
			opt('eventTextColor');
		var statements = [];
		if (backgroundColor) {
			statements.push('background-color:' + backgroundColor);
		}
		if (borderColor) {
			statements.push('border-color:' + borderColor);
		}
		if (textColor) {
			statements.push('color:' + textColor);
		}
		return statements.join(';');
	}

	function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
		for (var i=0; i<segments.length; i++) {
			var segment = segments[i];
			var element = segment.element;
			if (element) {
				callback(segment, element, i);
			}
		}
	}

	function hsides(element, includeMargins) {
		return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
	}

	function hpadding(element) {
		return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
		       (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
	}

	function hborders(element) {
		return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
		       (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
	}

	function hmargins(element) {
		return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
		       (parseFloat($.css(element[0], 'marginRight', true)) || 0);
	}

	function compareDaySegments(a, b) {
		return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
			b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
			a.event.start - b.event.start || // if a tie, sort by event start date
			(a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
	}

	function arrayMax(a) {
		return Math.max.apply(Math, a);
	}

	function lazySegBind(container, segs, bindHandlers) {
		container.unbind('mouseover').mouseover(function(ev) {
			var parent=ev.target, e,
				i, seg;
			while (parent != this) {
				e = parent;
				parent = parent.parentNode;
			}
			if ((i = e._fci) !== undefined) {
				e._fci = undefined;
				seg = segs[i];
				bindHandlers(seg.event, seg.element, seg);
				$(ev.target).trigger(ev);
			}
			ev.stopPropagation();
		});
	}

	function isDaySegmentCollision(segment, otherSegments) {
		for (var i=0; i<otherSegments.length; i++) {
			var otherSegment = otherSegments[i];
			if (
				otherSegment.leftCol <= segment.rightCol &&
				otherSegment.rightCol >= segment.leftCol
			) {
				return true;
			}
		}
		return false;
	}

	function hexToRgb(hex) {
		var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
		
		return result ? {
			r: parseInt(result[1], 16),
			g: parseInt(result[2], 16),
			b: parseInt(result[3], 16)
		} : null;
	}

})(jQuery);