
/* An abstract class comprised of a "grid" of cells that each represent a specific datetime
----------------------------------------------------------------------------------------------------------------------*/

var Grid = fc.Grid = RowRenderer.extend({

	start: null, // the date of the first cell
	end: null, // the date after the last cell

	rowCnt: 0, // number of rows
	colCnt: 0, // number of cols
	rowData: null, // array of objects, holding misc data for each row
	colData: null, // array of objects, holding misc data for each column

	el: null, // the containing element
	coordMap: null, // a GridCoordMap that converts pixel values to datetimes
	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.

	externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)

	// derived from options
	colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
	eventTimeFormat: null,
	displayEventTime: null,
	displayEventEnd: null,

	// if all cells are the same length of time, the duration they all share. optional.
	// when defined, allows the computeCellRange shortcut, as well as improved resizing behavior.
	cellDuration: null,

	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
	// of the date cells. if not defined, assumes to be day and time granularity.
	largeUnit: null,


	constructor: function() {
		RowRenderer.apply(this, arguments); // call the super-constructor

		this.coordMap = new GridCoordMap(this);
		this.elsByFill = {};
		this.externalDragStartProxy = proxy(this, 'externalDragStart');
	},


	/* Options
	------------------------------------------------------------------------------------------------------------------*/


	// Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
	// TODO: move to another class. not applicable to all Grids
	computeColHeadFormat: function() {
		// subclasses must implement if they want to use headHtml()
	},


	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
	computeEventTimeFormat: function() {
		return this.view.opt('smallTimeFormat');
	},


	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
	// Only applies to non-all-day events.
	computeDisplayEventTime: function() {
		return true;
	},


	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
	computeDisplayEventEnd: function() {
		return true;
	},


	/* Dates
	------------------------------------------------------------------------------------------------------------------*/


	// Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system.
	setRange: function(range) {
		var view = this.view;
		var displayEventTime;
		var displayEventEnd;

		this.start = range.start.clone();
		this.end = range.end.clone();

		this.rowData = [];
		this.colData = [];
		this.updateCells();

		// Populate option-derived settings. Look for override first, then compute if necessary.
		this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();

		this.eventTimeFormat =
			view.opt('eventTimeFormat') ||
			view.opt('timeFormat') || // deprecated
			this.computeEventTimeFormat();

		displayEventTime = view.opt('displayEventTime');
		if (displayEventTime == null) {
			displayEventTime = this.computeDisplayEventTime(); // might be based off of range
		}

		displayEventEnd = view.opt('displayEventEnd');
		if (displayEventEnd == null) {
			displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
		}

		this.displayEventTime = displayEventTime;
		this.displayEventEnd = displayEventEnd;
	},


	// Responsible for setting rowCnt/colCnt and any other row/col data
	updateCells: function() {
		// subclasses must implement
	},


	// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
	rangeToSegs: function(range) {
		// subclasses must implement
	},


	// Diffs the two dates, returning a duration, based on granularity of the grid
	diffDates: function(a, b) {
		if (this.largeUnit) {
			return diffByUnit(a, b, this.largeUnit);
		}
		else {
			return diffDayTime(a, b);
		}
	},


	/* Cells
	------------------------------------------------------------------------------------------------------------------*/
	// NOTE: columns are ordered left-to-right


	// Gets an object containing row/col number, misc data, and range information about the cell.
	// Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
	getCell: function(row, col) {
		var cell;

		if (col == null) {
			if (typeof row === 'number') { // a single-number offset
				col = row % this.colCnt;
				row = Math.floor(row / this.colCnt);
			}
			else { // an object with row/col properties
				col = row.col;
				row = row.row;
			}
		}

		cell = { row: row, col: col };

		$.extend(cell, this.getRowData(row), this.getColData(col));
		$.extend(cell, this.computeCellRange(cell));

		return cell;
	},


	// Given a cell object with index and misc data, generates a range object
	// If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does.
	// If being overridden, should return a range with reference-free date copies.
	computeCellRange: function(cell) {
		var date = this.computeCellDate(cell);

		return {
			start: date,
			end: date.clone().add(this.cellDuration)
		};
	},


	// Given a cell, returns its start date. Should return a reference-free date copy.
	computeCellDate: function(cell) {
		// subclasses can implement
	},


	// Retrieves misc data about the given row
	getRowData: function(row) {
		return this.rowData[row] || {};
	},


	// Retrieves misc data baout the given column
	getColData: function(col) {
		return this.colData[col] || {};
	},


	// Retrieves the element representing the given row
	getRowEl: function(row) {
		// subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
	},


	// Retrieves the element representing the given column
	getColEl: function(col) {
		// subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
	},


	// Given a cell object, returns the element that represents the cell's whole-day
	getCellDayEl: function(cell) {
		return this.getColEl(cell.col) || this.getRowEl(cell.row);
	},


	/* Cell Coordinates
	------------------------------------------------------------------------------------------------------------------*/


	// Computes the top/bottom coordinates of all rows.
	// By default, queries the dimensions of the element provided by getRowEl().
	computeRowCoords: function() {
		var items = [];
		var i, el;
		var top;

		for (i = 0; i < this.rowCnt; i++) {
			el = this.getRowEl(i);
			top = el.offset().top;
			items.push({
				top: top,
				bottom: top + el.outerHeight()
			});
		}

		return items;
	},


	// Computes the left/right coordinates of all rows.
	// By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL.
	computeColCoords: function() {
		var items = [];
		var i, el;
		var left;

		for (i = 0; i < this.colCnt; i++) {
			el = this.getColEl(i);
			left = el.offset().left;
			items.push({
				left: left,
				right: left + el.outerWidth()
			});
		}

		return items;
	},


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


	// Sets the container element that the grid should render inside of.
	// Does other DOM-related initializations.
	setElement: function(el) {
		var _this = this;

		this.el = el;

		// attach a handler to the grid's root element.
		// jQuery will take care of unregistering them when removeElement gets called.
		el.on('mousedown', function(ev) {
			if (
				!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
				!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
			) {
				_this.dayMousedown(ev);
			}
		});

		// attach event-element-related handlers. in Grid.events
		// same garbage collection note as above.
		this.bindSegHandlers();

		this.bindGlobalHandlers();
	},


	// Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
	// DOES NOT remove any content before hand (doens't clear events or call destroyDates), unlike View
	removeElement: function() {
		this.unbindGlobalHandlers();

		this.el.remove();

		// NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
	},


	// Renders the basic structure of grid view before any content is rendered
	renderSkeleton: function() {
		// subclasses should implement
	},


	// Renders the grid's date-related content (like cells that represent days/times).
	// Assumes setRange has already been called and the skeleton has already been rendered.
	renderDates: function() {
		// subclasses should implement
	},


	// Unrenders the grid's date-related content
	destroyDates: function() {
		// subclasses should implement
	},


	/* Handlers
	------------------------------------------------------------------------------------------------------------------*/


	// Binds DOM handlers to elements that reside outside the grid, such as the document
	bindGlobalHandlers: function() {
		$(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
	},


	// Unbinds DOM handlers from elements that reside outside the grid
	unbindGlobalHandlers: function() {
		$(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
	},


	// Process a mousedown on an element that represents a day. For day clicking and selecting.
	dayMousedown: function(ev) {
		var _this = this;
		var view = this.view;
		var isSelectable = view.opt('selectable');
		var dayClickCell; // null if invalid dayClick
		var selectionRange; // null if invalid selection

		// this listener tracks a mousedown on a day element, and a subsequent drag.
		// if the drag ends on the same day, it is a 'dayClick'.
		// if 'selectable' is enabled, this listener also detects selections.
		var dragListener = new CellDragListener(this.coordMap, {
			//distance: 5, // needs more work if we want dayClick to fire correctly
			scroll: view.opt('dragScroll'),
			dragStart: function() {
				view.unselect(); // since we could be rendering a new selection, we want to clear any old one
			},
			cellOver: function(cell, isOrig, origCell) {
				if (origCell) { // click needs to have started on a cell
					dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
					if (isSelectable) {
						selectionRange = _this.computeSelection(origCell, cell);
						if (selectionRange) {
							_this.renderSelection(selectionRange);
						}
						else {
							disableCursor();
						}
					}
				}
			},
			cellOut: function(cell) {
				dayClickCell = null;
				selectionRange = null;
				_this.destroySelection();
				enableCursor();
			},
			listenStop: function(ev) {
				if (dayClickCell) {
					view.trigger('dayClick', _this.getCellDayEl(dayClickCell), dayClickCell.start, ev);
				}
				if (selectionRange) {
					// the selection will already have been rendered. just report it
					view.reportSelection(selectionRange, ev);
				}
				enableCursor();
			}
		});

		dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
	},


	/* Event Helper
	------------------------------------------------------------------------------------------------------------------*/
	// TODO: should probably move this to Grid.events, like we did event dragging / resizing


	// Renders a mock event over the given range
	renderRangeHelper: function(range, sourceSeg) {
		var fakeEvent = this.fabricateHelperEvent(range, sourceSeg);

		this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
	},


	// Builds a fake event given a date range it should cover, and a segment is should be inspired from.
	// The range's end can be null, in which case the mock event that is rendered will have a null end time.
	// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
	fabricateHelperEvent: function(range, sourceSeg) {
		var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible

		fakeEvent.start = range.start.clone();
		fakeEvent.end = range.end ? range.end.clone() : null;
		fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange
		this.view.calendar.normalizeEventRange(fakeEvent);

		// this extra className will be useful for differentiating real events from mock events in CSS
		fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');

		// if something external is being dragged in, don't render a resizer
		if (!sourceSeg) {
			fakeEvent.editable = false;
		}

		return fakeEvent;
	},


	// Renders a mock event
	renderHelper: function(event, sourceSeg) {
		// subclasses must implement
	},


	// Unrenders a mock event
	destroyHelper: function() {
		// subclasses must implement
	},


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


	// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
	renderSelection: function(range) {
		this.renderHighlight(range);
	},


	// Unrenders any visual indications of a selection. Will unrender a highlight by default.
	destroySelection: function() {
		this.destroyHighlight();
	},


	// Given the first and last cells of a selection, returns a range object.
	// Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
	// Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
	computeSelection: function(firstCell, lastCell) {
		var dates = [
			firstCell.start,
			firstCell.end,
			lastCell.start,
			lastCell.end
		];
		var range;

		dates.sort(compareNumbers); // sorts chronologically. works with Moments

		range = {
			start: dates[0].clone(),
			end: dates[3].clone()
		};

		if (!this.view.calendar.isSelectionRangeAllowed(range)) {
			return null;
		}

		return range;
	},


	/* Highlight
	------------------------------------------------------------------------------------------------------------------*/


	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
	renderHighlight: function(range) {
		this.renderFill('highlight', this.rangeToSegs(range));
	},


	// Unrenders the emphasis on a date range
	destroyHighlight: function() {
		this.destroyFill('highlight');
	},


	// Generates an array of classNames for rendering the highlight. Used by the fill system.
	highlightSegClasses: function() {
		return [ 'fc-highlight' ];
	},


	/* Fill System (highlight, background events, business hours)
	------------------------------------------------------------------------------------------------------------------*/


	// Renders a set of rectangles over the given segments of time.
	// Returns a subset of segs, the segs that were actually rendered.
	// Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
	renderFill: function(type, segs) {
		// subclasses must implement
	},


	// Unrenders a specific type of fill that is currently rendered on the grid
	destroyFill: function(type) {
		var el = this.elsByFill[type];

		if (el) {
			el.remove();
			delete this.elsByFill[type];
		}
	},


	// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
	// Only returns segments that successfully rendered.
	// To be harnessed by renderFill (implemented by subclasses).
	// Analagous to renderFgSegEls.
	renderFillSegEls: function(type, segs) {
		var _this = this;
		var segElMethod = this[type + 'SegEl'];
		var html = '';
		var renderedSegs = [];
		var i;

		if (segs.length) {

			// build a large concatenation of segment HTML
			for (i = 0; i < segs.length; i++) {
				html += this.fillSegHtml(type, segs[i]);
			}

			// Grab individual elements from the combined HTML string. Use each as the default rendering.
			// Then, compute the 'el' for each segment.
			$(html).each(function(i, node) {
				var seg = segs[i];
				var el = $(node);

				// allow custom filter methods per-type
				if (segElMethod) {
					el = segElMethod.call(_this, seg, el);
				}

				if (el) { // custom filters did not cancel the render
					el = $(el); // allow custom filter to return raw DOM node

					// correct element type? (would be bad if a non-TD were inserted into a table for example)
					if (el.is(_this.fillSegTag)) {
						seg.el = el;
						renderedSegs.push(seg);
					}
				}
			});
		}

		return renderedSegs;
	},


	fillSegTag: 'div', // subclasses can override


	// Builds the HTML needed for one fill segment. Generic enought o work with different types.
	fillSegHtml: function(type, seg) {

		// custom hooks per-type
		var classesMethod = this[type + 'SegClasses'];
		var cssMethod = this[type + 'SegCss'];

		var classes = classesMethod ? classesMethod.call(this, seg) : [];
		var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});

		return '<' + this.fillSegTag +
			(classes.length ? ' class="' + classes.join(' ') + '"' : '') +
			(css ? ' style="' + css + '"' : '') +
			' />';
	},


	/* Generic rendering utilities for subclasses
	------------------------------------------------------------------------------------------------------------------*/


	// Renders a day-of-week header row.
	// TODO: move to another class. not applicable to all Grids
	headHtml: function() {
		return '' +
			'<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
				'<table>' +
					'<thead>' +
						this.rowHtml('head') + // leverages RowRenderer
					'</thead>' +
				'</table>' +
			'</div>';
	},


	// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
	// TODO: move to another class. not applicable to all Grids
	headCellHtml: function(cell) {
		var view = this.view;
		var date = cell.start;

		return '' +
			'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
				htmlEscape(date.format(this.colHeadFormat)) +
			'</th>';
	},


	// Renders the HTML for a single-day background cell
	bgCellHtml: function(cell) {
		var view = this.view;
		var date = cell.start;
		var classes = this.getDayClasses(date);

		classes.unshift('fc-day', view.widgetContentClass);

		return '<td class="' + classes.join(' ') + '"' +
			' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
			'></td>';
	},


	// Computes HTML classNames for a single-day cell
	getDayClasses: function(date) {
		var view = this.view;
		var today = view.calendar.getNow().stripTime();
		var classes = [ 'fc-' + dayIDs[date.day()] ];

		if (
			view.intervalDuration.as('months') == 1 &&
			date.month() != view.intervalStart.month()
		) {
			classes.push('fc-other-month');
		}

		if (date.isSame(today, 'day')) {
			classes.push(
				'fc-today',
				view.highlightStateClass
			);
		}
		else if (date < today) {
			classes.push('fc-past');
		}
		else {
			classes.push('fc-future');
		}

		return classes;
	}

});
