define([
	'dojo/_base/declare',
	'dojo/_base/array',
	'dojo/on',
	'dojo/query',
	'dojo/_base/lang',
	'dojo/dom',
	'dojo/dom-construct',
	'dojo/dom-geometry',
	'dojo/has',
	'../util/misc',
	'dojo/_base/html'
], function (declare, arrayUtil, listen, query, lang, dom, domConstruct, geom, has, miscUtil) {

	function addRowSpan(table, span, startRow, column, id) {
		// loop through the rows of the table and add this column's id to
		// the rows' column
		for (var i = 1; i < span; i++) {
			table[startRow + i][column] = id;
		}
	}
	function subRowAssoc(subRows) {
		// Take a sub-row structure and output an object with key=>value pairs
		// The keys will be the column id's; the values will be the first-row column
		// that column's resizer should be associated with.

		var i = subRows.length,
			l = i,
			numCols = arrayUtil.filter(subRows[0], function (column) {
				return !column.hidden;
			}).length,
			table = new Array(i);

		// create table-like structure in an array so it can be populated
		// with row-spans and col-spans
		while (i--) {
			table[i] = new Array(numCols);
		}

		var associations = {};

		for (i = 0; i < l; i++) {
			var row = table[i],
				subRow = subRows[i];

			// j: counter for table columns
			// js: counter for subrow structure columns
			for (var j = 0, js = 0; j < numCols; j++) {
				var cell = subRow[js], k;

				// if something already exists in the table (row-span), skip this
				// spot and go to the next
				if (typeof row[j] !== 'undefined') {
					continue;
				}
				row[j] = cell.id;

				if (cell.rowSpan && cell.rowSpan > 1) {
					addRowSpan(table, cell.rowSpan, i, j, cell.id);
				}

				// colSpans are only applicable in the second or greater rows
				// and only if the colSpan is greater than 1
				if (i > 0 && cell.colSpan && cell.colSpan > 1) {
					for (k = 1; k < cell.colSpan; k++) {
						// increment j and assign the id since this is a span
						row[++j] = cell.id;
						if (cell.rowSpan && cell.rowSpan > 1) {
							addRowSpan(table, cell.rowSpan, i, j, cell.id);
						}
					}
				}
				associations[cell.id] = subRows[0][j].id;
				js++;
			}
		}

		return associations;
	}

	function resizeColumnWidth(grid, colId, width, parentType, doResize) {
		// don't react to widths <= 0, e.g. for hidden columns
		if (width <= 0) {
			return;
		}

		var column = grid.columns[colId],
			event,
			rule;

		if (!column) {
			return;
		}

		event = {
			grid: grid,
			columnId: colId,
			width: width,
			bubbles: true,
			cancelable: true
		};

		if (parentType) {
			event.parentType = parentType;
		}

		if (!grid._resizedColumns || listen.emit(grid.headerNode, 'dgrid-columnresize', event)) {
			// Update width on column object, then convert value for CSS
			if (width === 'auto') {
				delete column.width;
			}
			else {
				column.width = width;
				width += 'px';
			}

			rule = grid._columnSizes[colId];

			if (rule) {
				// Modify existing, rather than deleting + adding
				rule.set('width', width);
			}
			else {
				// Use miscUtil function directly, since we clean these up ourselves anyway
				rule = miscUtil.addCssRule('#' + miscUtil.escapeCssIdentifier(grid.domNode.id) +
					' .dgrid-column-' + miscUtil.escapeCssIdentifier(colId, '-'),
					'width: ' + width + ';');
			}

			// keep a reference for future removal
			grid._columnSizes[colId] = rule;

			if (doResize !== false) {
				grid.resize();
			}

			return true;
		}
	}

	// Functions for shared resizer node

	var resizerNode, // DOM node for resize indicator, reused between instances
		resizerGuardNode, // DOM node to guard against clicks registering on header cells (and inducing sort)
		resizableCount = 0; // Number of ColumnResizer-enabled grid instances
	var resizer = {
		// This object contains functions for manipulating the shared resizerNode
		create: function () {
			resizerNode = domConstruct.create('div', { className: 'dgrid-column-resizer' });
			resizerGuardNode = domConstruct.create('div', { className: 'dgrid-resize-guard' });
		},
		destroy: function () {
			domConstruct.destroy(resizerNode);
			domConstruct.destroy(resizerGuardNode);
			resizerNode = resizerGuardNode = null;
		},
		show: function (grid) {
			var pos = geom.position(grid.domNode, true);
			resizerNode.style.top = pos.y + 'px';
			resizerNode.style.height = pos.h + 'px';
			document.body.appendChild(resizerNode);
			grid.domNode.appendChild(resizerGuardNode);
		},
		move: function (x) {
			resizerNode.style.left = x + 'px';
		},
		hide: function () {
			resizerNode.parentNode.removeChild(resizerNode);
			resizerGuardNode.parentNode.removeChild(resizerGuardNode);
		}
	};

	return declare(null, {
		resizeNode: null,

		// minWidth: Number
		//		Minimum column width, in px.
		minWidth: 40,

		// adjustLastColumn: Boolean
		//		If true, adjusts the last column's width to "auto" at times where the
		//		browser would otherwise stretch all columns to span the grid.
		adjustLastColumn: true,

		_resizedColumns: false, // flag indicating if resizer has converted column widths to px

		buildRendering: function () {
			this.inherited(arguments);

			// Create resizerNode when first grid w/ ColumnResizer is created
			if (!resizableCount) {
				resizer.create();
			}
			resizableCount++;
		},

		destroy: function () {
			this.inherited(arguments);

			// Remove any applied column size styles since we're tracking them directly
			for (var name in this._columnSizes) {
				this._columnSizes[name].remove();
			}

			// If this is the last grid on the page with ColumnResizer, destroy the
			// shared resizerNode
			if (!--resizableCount) {
				resizer.destroy();
			}
		},

		resizeColumnWidth: function (colId, width) {
			// Summary:
			//      calls grid's styleColumn function to add a style for the column
			// colId: String
			//      column id
			// width: Integer
			//      new width of the column
			return resizeColumnWidth(this, colId, width);
		},

		configStructure: function () {
			var oldSizes = this._oldColumnSizes = lang.mixin({}, this._columnSizes), // shallow clone
				k;

			this._resizedColumns = false;
			this._columnSizes = {};

			this.inherited(arguments);

			// Remove old column styles that are no longer relevant; this is specifically
			// done *after* calling inherited so that _columnSizes will contain keys
			// for all columns in the new structure that were assigned widths.
			for (k in oldSizes) {
				if (!(k in this._columnSizes)) {
					oldSizes[k].remove();
				}
			}
			delete this._oldColumnSizes;
		},

		_configColumn: function (column) {
			this.inherited(arguments);

			var colId = column.id,
				rule;

			if ('width' in column) {
				// Update or add a style rule for the specified width
				if ((rule = this._oldColumnSizes[colId])) {
					rule.set('width', column.width + 'px');
				}
				else {
					rule = miscUtil.addCssRule('#' + miscUtil.escapeCssIdentifier(this.domNode.id) +
						' .dgrid-column-' + miscUtil.escapeCssIdentifier(colId, '-'),
						'width: ' + column.width + 'px;');
				}
				this._columnSizes[colId] = rule;
			}
		},

		renderHeader: function () {
			this.inherited(arguments);

			var grid = this;

			var assoc;
			if (this.columnSets && this.columnSets.length) {
				var csi = this.columnSets.length;
				while (csi--) {
					assoc = lang.mixin(assoc || {}, subRowAssoc(this.columnSets[csi]));
				}
			}
			else if (this.subRows && this.subRows.length > 1) {
				assoc = subRowAssoc(this.subRows);
			}

			var colNodes = query('.dgrid-cell', grid.headerNode),
				i = colNodes.length;
			while (i--) {
				var colNode = colNodes[i],
					id = colNode.columnId,
					col = grid.columns[id],
					childNodes = colNode.childNodes,
					resizeHandle;

				if (!col || col.resizable === false) {
					continue;
				}

				var headerTextNode = domConstruct.create('div', { className: 'dgrid-resize-header-container' });
				colNode.contents = headerTextNode;

				// move all the children to the header text node
				while (childNodes.length > 0) {
					headerTextNode.appendChild(childNodes[0]);
				}

				resizeHandle = domConstruct.create('div', {
					className: 'dgrid-resize-handle resizeNode-' + miscUtil.escapeCssIdentifier(id, '-')
				}, headerTextNode);
				colNode.appendChild(headerTextNode);
				resizeHandle.columnId = assoc && assoc[id] || id;
			}

			if (!grid.mouseMoveListen) {
				// establish listeners for initiating, dragging, and finishing resize
				listen(grid.headerNode,
					'.dgrid-resize-handle:mousedown' +
						(has('touch') ? ',.dgrid-resize-handle:touchstart' : ''),
					function (e) {
						grid._resizeMouseDown(e, this);
						grid.mouseMoveListen.resume();
						grid.mouseUpListen.resume();
					}
				);
				grid._listeners.push(grid.mouseMoveListen =
					listen.pausable(document,
						'mousemove' + (has('touch') ? ',touchmove' : ''),
						miscUtil.throttleDelayed(function (e) {
							grid._updateResizerPosition(e);
						})
				));
				grid._listeners.push(grid.mouseUpListen = listen.pausable(document,
					'mouseup' + (has('touch') ? ',touchend' : ''),
					function (e) {
						grid._resizeMouseUp(e);
						grid.mouseMoveListen.pause();
						grid.mouseUpListen.pause();
					}
				));
				// initially pause the move/up listeners until a drag happens
				grid.mouseMoveListen.pause();
				grid.mouseUpListen.pause();
			}
		}, // end renderHeader

		_resizeMouseDown: function (e, target) {
			// Summary:
			//      called when mouse button is pressed on the header
			// e: Object
			//      mousedown event object

			// preventDefault actually seems to be enough to prevent browser selection
			// in all but IE < 9.  setSelectable works for those.
			e.preventDefault();
			dom.setSelectable(this.domNode, false);

			this._startX = this._getResizeMouseLocation(e); //position of the target

			this._targetCell = query('.dgrid-column-' + miscUtil.escapeCssIdentifier(target.columnId, '-'),
				this.headerNode)[0];

			// Show resizerNode after initializing its x position
			this._updateResizerPosition(e);
			resizer.show(this);
		},
		_resizeMouseUp: function (e) {
			// Summary:
			//      called when mouse button is released
			// e: Object
			//      mouseup event object

			var columnSizes = this._columnSizes,
				colNodes, colWidths, gridWidth;

			if (this.adjustLastColumn) {
				// For some reason, total column width needs to be 1 less than this
				gridWidth = this.headerNode.clientWidth - 1;
			}

			//This is used to set all the column widths to a static size
			if (!this._resizedColumns) {
				colNodes = query('.dgrid-cell', this.headerNode);

				if (this.columnSets && this.columnSets.length) {
					colNodes = colNodes.filter(function (node) {
						var idx = node.columnId.split('-');
						return idx[0] === '0' && !(node.columnId in columnSizes);
					});
				}
				else if (this.subRows && this.subRows.length > 1) {
					colNodes = colNodes.filter(function (node) {
						return node.columnId.charAt(0) === '0' && !(node.columnId in columnSizes);
					});
				}

				// Get a set of sizes before we start mutating, to avoid
				// weird disproportionate measures if the grid has set
				// column widths, but no full grid width set
				colWidths = colNodes.map(function (colNode) {
					return colNode.offsetWidth;
				});

				// Set a baseline size for each column based on
				// its original measure
				colNodes.forEach(function (colNode, i) {
					resizeColumnWidth(this, colNode.columnId, colWidths[i], null, false);
				}, this);

				this._resizedColumns = true;
			}
			dom.setSelectable(this.domNode, true);

			var cell = this._targetCell,
				delta = this._getResizeMouseLocation(e) - this._startX, //final change in position of resizer
				newWidth = cell.offsetWidth + delta, //the new width after resize
				obj = this._getResizedColumnWidths(),//get current total column widths before resize
				totalWidth = obj.totalWidth,
				lastCol = obj.lastColId,
				lastColWidth = query('.dgrid-column-' + miscUtil.escapeCssIdentifier(lastCol, '-'),
					this.headerNode)[0].offsetWidth;

			if (newWidth < this.minWidth) {
				//enforce minimum widths
				newWidth = this.minWidth;
			}

			if (resizeColumnWidth(this, cell.columnId, newWidth, e.type)) {
				if (cell.columnId !== lastCol && this.adjustLastColumn) {
					if (totalWidth + delta < gridWidth) {
						//need to set last column's width to auto
						resizeColumnWidth(this, lastCol, 'auto', e.type);
					}
					else if (lastColWidth - delta <= this.minWidth) {
						//change last col width back to px, unless it is the last column itself being resized...
						resizeColumnWidth(this, lastCol, this.minWidth, e.type);
					}
				}
			}
			resizer.hide();

			// Clean up after the resize operation
			delete this._startX;
			delete this._targetCell;
		},

		_updateResizerPosition: function (e) {
			// Summary:
			//      updates position of resizer bar as mouse moves
			// e: Object
			//      mousemove event object

			if (!this._targetCell) {
				return; // Release event was already processed
			}

			var mousePos = this._getResizeMouseLocation(e),
				delta = mousePos - this._startX, //change from where user clicked to where they drag
				width = this._targetCell.offsetWidth,
				left = mousePos;
			if (width + delta < this.minWidth) {
				left = this._startX - (width - this.minWidth);
			}
			resizer.move(left);
		},

		_getResizeMouseLocation: function (e) {
			//Summary:
			//      returns position of mouse relative to the left edge
			// e: event object
			//      mouse move event object
			var posX = 0;
			if (e.pageX) {
				posX = e.pageX;
			}
			else if (e.clientX) {
				posX = e.clientX + document.body.scrollLeft +
					document.documentElement.scrollLeft;
			}
			return posX;
		},
		_getResizedColumnWidths: function () {
			//Summary:
			//      returns object containing new column width and column id
			var totalWidth = 0,
				colNodes = query(
					(this.columnSets ? '.dgrid-column-set-cell ' : '') + 'tr:first-child .dgrid-cell',
					this.headerNode);

			var i = colNodes.length;
			if (!i) {
				return {};
			}

			var lastColId = colNodes[i - 1].columnId;

			while (i--) {
				totalWidth += colNodes[i].offsetWidth;
			}
			return {totalWidth: totalWidth, lastColId: lastColId};
		}
	});
});
