define([
	'dojo/_base/declare',
	'dojo/_base/lang',
	'dojo/Deferred',
	'dojo/dom-construct',
	'dojo/dom-class',
	'dojo/on',
	'dojo/has',
	'dojo/query',
	'./Grid',
	'dojo/_base/sniff'
], function (declare, lang, Deferred, domConstruct, domClass, on, has, query, Grid) {

	return declare(null, {
		constructor: function () {
			this._editorInstances = {};
			// Tracks shared editor dismissal listeners, and editor click/change listeners for old IE
			this._editorColumnListeners = [];
			// Tracks always-on editor listeners for old IE, or listeners for triggering shared editors
			this._editorCellListeners = {};
			this._editorsPendingStartup = [];
		},

		postCreate: function () {
			var self = this;

			this.inherited(arguments);

			this.on('.dgrid-input:focusin', function () {
				self._focusedEditorCell = self.cell(this);
			});
			this._editorFocusoutHandle = on.pausable(this.domNode, '.dgrid-input:focusout', function () {
				self._focusedEditorCell = null;
			});
			this._listeners.push(this._editorFocusoutHandle);
		},

		insertRow: function () {
			this._editorRowListeners = {};
			var rowElement = this.inherited(arguments);
			var row = this.row(rowElement);
			var rowListeners = this._editorCellListeners[rowElement.id] =
				this._editorCellListeners[rowElement.id] || {};

			for (var key in this._editorRowListeners) {
				rowListeners[key] = this._editorRowListeners[key];
			}
			// Null this out so that _createEditor can tell whether the editor being created is
			// an individual cell editor at insertion time, vs. a cell being refreshed
			this._editorRowListeners = null;

			var previouslyFocusedCell = this._previouslyFocusedEditorCell;

			if (previouslyFocusedCell && previouslyFocusedCell.row.id === row.id) {
				this.edit(this.cell(row, previouslyFocusedCell.column.id));
			}
			return rowElement;
		},

		refresh: function () {
			for (var id in this._editorInstances) {
				var editorInstanceDomNode = this._editorInstances[id].domNode;
				if (editorInstanceDomNode && editorInstanceDomNode.parentNode) {
					// Remove any editor widgets from the DOM before List destroys it, to avoid issues in IE (#1100)
					editorInstanceDomNode.parentNode.removeChild(editorInstanceDomNode);
				}
			}

			return this.inherited(arguments);
		},

		removeRow: function (rowElement) {
			var self = this;
			var focusedCell = this._focusedEditorCell;

			if (focusedCell && focusedCell.row.id === this.row(rowElement).id) {
				this._previouslyFocusedEditorCell = focusedCell;
				// Pause the focusout handler until after this row has had
				// time to re-render, if this removal is part of an update.
				// A setTimeout is used here instead of resuming in insertRow,
				// since if a row were actually removed (not updated) while
				// editing, the handler would not be properly hooked up again
				// for future occurrences.
				this._editorFocusoutHandle.pause();
				setTimeout(function () {
					self._editorFocusoutHandle.resume();
					self._previouslyFocusedEditorCell = null;
				}, 0);
			}

			if (this._editorCellListeners[rowElement.id]) {
				for (var columnId in this._editorCellListeners[rowElement.id]) {
					this._editorCellListeners[rowElement.id][columnId].remove();
				}
				delete this._editorCellListeners[rowElement.id];
			}

			for (var i = this._alwaysOnWidgetColumns.length; i--;) {
				// Destroy always-on editor widgets during the row removal operation,
				// but don't trip over loading nodes from incomplete requests
				var cellElement = this.cell(rowElement, this._alwaysOnWidgetColumns[i].id).element,
					widget = cellElement && (cellElement.contents || cellElement).widget;
				if (widget) {
					this._editorFocusoutHandle.pause();
					widget.destroyRecursive();
				}
			}

			return this.inherited(arguments);
		},

		renderArray: function () {
			var rows = this.inherited(arguments);
			if (rows.length) {
				// Finish processing any pending editors that are now displayed
				this._startupPendingEditors();
			}
			else {
				this._editorsPendingStartup = [];
			}
			return rows;
		},

		_onNotification: function () {
			this.inherited(arguments);
			this._startupPendingEditors();
		},

		_destroyColumns: function () {
			this._editorStructureCleanup();
			this.inherited(arguments);
		},

		_editorStructureCleanup: function () {
			var editorInstances = this._editorInstances;
			var listeners = this._editorColumnListeners;

			if (this._editTimer) {
				clearTimeout(this._editTimer);
			}
			// Do any clean up of previous column structure.
			for (var columnId in editorInstances) {
				var editor = editorInstances[columnId];
				if (editor.domNode) {
					// The editor is a widget
					editor.destroyRecursive();
				}
			}
			this._editorInstances = {};

			for (var i = listeners.length; i--;) {
				listeners[i].remove();
			}

			for (var rowId in this._editorCellListeners) {
				for (columnId in this._editorCellListeners[rowId]) {
					this._editorCellListeners[rowId][columnId].remove();
				}
			}

			for (i = 0; i < this._editorColumnListeners.length; i++) {
				this._editorColumnListeners[i].remove();
			}

			this._editorCellListeners = {};
			this._editorColumnListeners = [];
			this._editorsPendingStartup = [];
		},

		_configColumns: function () {
			var columnArray = this.inherited(arguments);
			this._alwaysOnWidgetColumns = [];
			for (var i = 0, l = columnArray.length; i < l; i++) {
				if (columnArray[i].editor) {
					this._configureEditorColumn(columnArray[i]);
				}
			}
			return columnArray;
		},

		_configureEditorColumn: function (column) {
			// summary:
			//		Adds editing capability to a column's cells.

			var editor = column.editor;
			var self = this;

			var originalRenderCell = column.renderCell || this._defaultRenderCell;
			var editOn = column.editOn;
			var isWidget = typeof editor !== 'string';

			if (editOn) {
				// Create one shared widget/input to be swapped into the active cell.
				this._editorInstances[column.id] = this._createSharedEditor(column, originalRenderCell);
			}
			else if (isWidget) {
				// Append to array iterated in removeRow
				this._alwaysOnWidgetColumns.push(column);
			}

			column.renderCell = editOn ? function (object, value, cell, options) {
				// TODO: Consider using event delegation
				// (Would require using dgrid's focus events for activating on focus,
				// which we already advocate in docs for optimal use)

				if (!options || !options.alreadyHooked) {
					var listener = on(cell, editOn, function () {
						self._activeOptions = options;
						self.edit(this);
					});
					if (self._editorRowListeners) {
						self._editorRowListeners[column.id] = listener;
					}
					else {
						// We're in refreshCell since _editorRowListeners doesn't exist,
						// so the row should exist
						var row = self.row(object);
						self._editorCellListeners[row.element.id][column.id] = listener;
					}
				}

				// initially render content in non-edit mode
				return originalRenderCell.call(column, object, value, cell, options);

			} : function (object, value, cell, options) {
				// always-on: create editor immediately upon rendering each cell
				if (!column.canEdit || column.canEdit(object, value)) {
					// _createEditor also needs the object for when this is invoked via refreshCell, to get the row
					var cmp = self._createEditor(column, object);
					self._showEditor(cmp, column, cell, value);
					// Maintain reference for later use.
					cell[isWidget ? 'widget' : 'input'] = cmp;
				}
				else {
					return originalRenderCell.call(column, object, value, cell, options);
				}
			};
		},

		edit: function (cell) {
			// summary:
			//		Shows/focuses the editor for a given grid cell.
			// cell: Object
			//		Cell (or something resolvable by grid.cell) to activate editor on.
			// returns:
			//		If the cell is editable, returns a promise resolving to the editor
			//		input/widget when the cell editor is focused.
			//		If the cell is not editable, returns null.

			var self = this;
			var column;
			var cellElement;
			var dirty;
			var field;
			var value;
			var cmp;
			var dfd;

			function showEditor(dfd) {
				self._activeCell = cellElement;
				self._showEditor(cmp, column, cellElement, value);

				// focus / blur-handler-resume logic is surrounded in a setTimeout
				// to play nice with Keyboard's dgrid-cellfocusin as an editOn event
				self._editTimer = setTimeout(function () {
					// focus the newly-placed control (supported by form widgets and HTML inputs)
					if (cmp.focus) {
						cmp.focus();
					}
					// resume blur handler once editor is focused
					if (column._editorBlurHandle) {
						column._editorBlurHandle.resume();
					}
					self._editTimer = null;
					dfd.resolve(cmp);
				}, 0);
			}

			if (!cell.column) {
				cell = this.cell(cell);
			}
			if (!cell || !cell.element) {
				return null;
			}

			column = cell.column;
			field = column.field;
			cellElement = cell.element.contents || cell.element;

			if ((cmp = this._editorInstances[column.id])) {
				// Shared editor (editOn used)
				if (this._activeCell !== cellElement) {
					// Get the cell value
					var row = cell.row;
					dirty = this.dirty && this.dirty[row.id];
					value = (dirty && field in dirty) ? dirty[field] :
						column.get ? column.get(row.data) : row.data[field];
					// Check to see if the cell can be edited
					if (!column.canEdit || column.canEdit(cell.row.data, value)) {
						dfd = new Deferred();

						// In some browsers, moving a DOM node causes a blur event to fire which in this case,
						// is a bad time for the blur handler to run.  Blur the input node first.
						var node = cmp.domNode || cmp;
						if (node.offsetWidth) {
							// The editor is visible.  Blur it.
							node.blur();
							// In IE, the blur does not complete immediately.
							// Push showing of the editor to the next turn.
							// (dfd will be resolved within showEditor)
							setTimeout(function () {
								showEditor(dfd);
							}, 0);
						} else {
							showEditor(dfd);
						}

						return dfd.promise;
					}
				}
			}
			else if (column.editor) {
				// editor but not shared; always-on
				cmp = cellElement.widget || cellElement.input;
				if (cmp) {
					dfd = new Deferred();
					if (cmp.focus) {
						cmp.focus();
					}
					dfd.resolve(cmp);
					return dfd.promise;
				}
			}
			return null;
		},

		refreshCell: function (cell) {
			var column = cell.column;
			var value = column.get ? column.get(cell.row.data) : cell.row.data[column.field];

			var editor;

			if (column.editor) {
				if (cell.column.editOn && this._activeCell === cell.element) {
					editor = this._editorInstances[cell.column.id];
				}
				else if (!cell.column.editOn) {
					editor = cell.element.widget || cell.element.input;
				}
			}

			if (editor) {
				if (editor.domNode) {
					editor.set('value', value);
				}
				else {
					this._updateInputValue(editor, value);
				}
				return (new Deferred()).resolve();
			}

			return this.inherited(arguments);
		},

		_showEditor: function (cmp, column, cellElement, value) {
			// Places a shared editor into the newly-active cell in the column.
			// Also called when rendering an editor in an "always-on" editor column.

			var isWidget = cmp.domNode;
			// for regular inputs, we can update the value before even showing it
			if (!isWidget) {
				this._updateInputValue(cmp, value);
			}

			cellElement.innerHTML = '';
			domClass.add(cellElement, 'dgrid-cell-editing');

			// If a shared editor is a validation widget, reset it to clear validation state
			// (The value will be preserved since it is explicitly set in _startupEditor)
			if (isWidget && column.editOn && cmp.validate && cmp.reset) {
				cmp.reset();
			}

			cellElement.appendChild(cmp.domNode || cmp);

			if (isWidget && !column.editOn) {
				// Queue arguments to be run once editor is in DOM
				this._editorsPendingStartup.push([cmp, column, cellElement, value]);
			}
			else {
				this._startupEditor(cmp, column, cellElement, value);
			}
		},

		_startupEditor: function (cmp, column, cellElement, value) {
			// summary:
			//		Handles editor widget startup logic and updates the editor's value.

			if (cmp.domNode) {
				// For widgets, ensure startup is called before setting value, to maximize compatibility
				// with flaky widgets like dijit/form/Select.
				if (!cmp._started) {
					cmp.startup();
				}

				// Set value, but ensure it isn't processed as a user-generated change.
				// (Clear flag on a timeout to wait for delayed onChange to fire first)
				cmp._dgridIgnoreChange = true;
				cmp.set('value', value);
				setTimeout(function () {
					cmp._dgridIgnoreChange = false;
				}, 0);
			}

			// track previous value for short-circuiting or in case we need to revert
			cmp._dgridLastValue = value;
			// if this is an editor with editOn, also update _activeValue
			// (_activeOptions will have been updated previously)
			if (this._activeCell) {
				this._activeValue = value;
				// emit an event immediately prior to placing a shared editor
				on.emit(cellElement, 'dgrid-editor-show', {
					grid: this,
					cell: this.cell(cellElement),
					column: column,
					editor: cmp,
					bubbles: true,
					cancelable: false
				});
			}
		},

		_startupPendingEditors: function () {
			var args = this._editorsPendingStartup;
			for (var i = args.length; i--;) {
				this._startupEditor.apply(this, args[i]);
			}
			this._editorsPendingStartup = [];
		},

		_handleEditorChange: function (evt, column) {
			var target = evt.target;
			if ('_dgridLastValue' in target && target.className.indexOf('dgrid-input') > -1) {
				this._updatePropertyFromEditor(column || this.cell(target).column, target, evt);
			}
		},

		_createEditor: function (column, object) {
			// Creates an editor instance based on column definition properties,
			// and hooks up events.
			var editor = column.editor,
				editOn = column.editOn,
				self = this,
				Widget = typeof editor !== 'string' && editor,
				args,
				cmp,
				node,
				tagName,
				tagArgs = {};

			args = column.editorArgs || {};
			if (typeof args === 'function') {
				args = args.call(this, column);
			}

			if (Widget) {
				cmp = new Widget(args);
				node = cmp.focusNode || cmp.domNode;

				// Add dgrid-input to className to make consistent with HTML inputs.
				node.className += ' dgrid-input';

				// For editOn editors, connect to onBlur rather than onChange, since
				// the latter is delayed by setTimeouts in Dijit and will fire too late.
				cmp.on(editOn ? 'blur' : 'change', function () {
					if (!cmp._dgridIgnoreChange) {
						self._updatePropertyFromEditor(column, this, {type: 'widget'});
					}
				});
			}
			else {
				// considerations for standard HTML form elements
				if (!this._hasInputListener) {
					// register one listener at the top level that receives events delegated
					this._hasInputListener = true;
					this.on('change', function (evt) {
						self._handleEditorChange(evt);
					});
					// also register a focus listener
				}

				if (editor === 'textarea') {
					tagName = 'textarea';
				}
				else {
					tagName = 'input';
					tagArgs.type = editor;
				}
				cmp = node = domConstruct.create(tagName, lang.mixin(tagArgs, {
					className: 'dgrid-input',
					name: column.field,
					tabIndex: isNaN(column.tabIndex) ? -1 : column.tabIndex
				}, args));

				if (has('ie') < 9) {
					// IE<9 doesn't fire change events for all the right things,
					// and it doesn't bubble.
					var listener;
					if (editor === 'radio' || editor === 'checkbox') {
						// listen for clicks since IE doesn't fire change events properly for checks/radios
						listener = on(cmp, 'click', function (evt) {
							self._handleEditorChange(evt, column);
						});
					}
					else {
						listener = on(cmp, 'change', function (evt) {
							self._handleEditorChange(evt, column);
						});
					}

					if (editOn) {
						// Shared editor handlers are maintained in _editorColumnListeners, since they're not per-row
						this._editorColumnListeners.push(listener);
					}
					else if (this._editorRowListeners) {
						this._editorRowListeners[column.id] = listener;
					}
					// If editRowListeners doesn't exist and this is an always-on editor,
					// then we're here from renderCell via refreshCell, and the row should exist
					else {
						this._editorCellListeners[this.row(object).element.id][column.id] = listener;
					}
				}
			}

			if (column.autoSelect) {
				var selectNode = cmp.focusNode || cmp;
				if (selectNode.select) {
					on(selectNode, 'focus', function () {
						// setTimeout is needed for always-on editors on WebKit,
						// otherwise selection is reset immediately afterwards
						setTimeout(function () {
							selectNode.select();
						}, 0);
					});
				}
			}

			return cmp;
		},

		_createSharedEditor: function (column) {
			// Creates an editor instance with additional considerations for
			// shared usage across an entire column (for columns with editOn specified).

			var cmp = this._createEditor(column),
				self = this,
				isWidget = cmp.domNode,
				node = cmp.domNode || cmp,
				focusNode = cmp.focusNode || node,
				reset = isWidget ?
					function () {
						cmp.set('value', cmp._dgridLastValue);
					} :
					function () {
						self._updateInputValue(cmp, cmp._dgridLastValue);
						// Update property again in case we need to revert a previous change
						self._updatePropertyFromEditor(column, cmp);
					};

			function blur() {
				var element = self._activeCell;
				focusNode.blur();

				if (typeof self.focus === 'function') {
					// Dijit form widgets don't end up dismissed until the next turn,
					// so wait before calling focus (otherwise Keyboard will focus the
					// input again).  IE<9 needs to wait longer, otherwise the cell loses
					// focus after we've set it.
					setTimeout(function () {
						self.focus(element);
					}, isWidget && has('ie') < 9 ? 15 : 0);
				}
			}

			function onblur() {
				var parentNode = node.parentNode,
					options = { alreadyHooked: true },
					cell = self.cell(node);

				// emit an event immediately prior to removing an editOn editor
				on.emit(cell.element, 'dgrid-editor-hide', {
					grid: self,
					cell: cell,
					column: column,
					editor: cmp,
					bubbles: true,
					cancelable: false
				});
				column._editorBlurHandle.pause();
				// Remove the editor from the cell, to be reused later.
				parentNode.removeChild(node);

				if (cell.row) {
					// If the row is still present (i.e. we didn't blur due to removal),
					// clear out the rest of the cell's contents, then re-render with new value.
					domClass.remove(cell.element, 'dgrid-cell-editing');
					domConstruct.empty(parentNode);
					Grid.appendIfNode(parentNode, column.renderCell(cell.row.data, self._activeValue, parentNode,
						self._activeOptions ? lang.delegate(options, self._activeOptions) : options));
				}

				// Reset state now that editor is deactivated;
				// reset _focusedEditorCell as well since some browsers will not
				// trigger the focusout event handler in this case
				self._focusedEditorCell = self._activeCell = self._activeValue = self._activeOptions = null;
			}

			function dismissOnKey(evt) {
				// Contains logic for reacting to enter/escape keypresses to save/cancel edits.
				// Calls `focusNode.blur()` in cases where field should be dismissed.
				var key = evt.keyCode || evt.which;

				if (key === 27) {
					// Escape: revert + dismiss
					reset();
					self._activeValue = cmp._dgridLastValue;
					blur();
				}
				else if (key === 13 && column.dismissOnEnter !== false) {
					// Enter: dismiss
					blur();
				}
			}

			// hook up enter/esc key handling
			this._editorColumnListeners.push(on(focusNode, 'keydown', dismissOnKey));

			// hook up blur handler, but don't activate until widget is activated
			(column._editorBlurHandle = on.pausable(cmp, 'blur', onblur)).pause();
			this._editorColumnListeners.push(column._editorBlurHandle);

			return cmp;
		},

		_updatePropertyFromEditor: function (column, cmp, triggerEvent) {
			var value,
				id,
				editedRow;

			if (!cmp.isValid || cmp.isValid()) {
				value = this._updateProperty((cmp.domNode || cmp).parentNode,
					this._activeCell ? this._activeValue : cmp._dgridLastValue,
					this._retrieveEditorValue(column, cmp), triggerEvent);

				if (this._activeCell) { // for editors with editOn defined
					this._activeValue = value;
				}
				else { // for always-on editors, update _dgridLastValue immediately
					cmp._dgridLastValue = value;
				}

				if (cmp.type === 'radio' && cmp.name && !column.editOn && column.field) {
					editedRow = this.row(cmp);

					// Update all other rendered radio buttons in the group
					query('input[type=radio][name=' + cmp.name + ']', this.contentNode).forEach(function (radioBtn) {
						var row = this.row(radioBtn);
						// Only update _dgridLastValue and the dirty data if it exists
						// and is not already false
						if (radioBtn !== cmp && radioBtn._dgridLastValue) {
							radioBtn._dgridLastValue = false;
							if (this.updateDirty) {
								this.updateDirty(row.id, column.field, false);
							}
							else {
								// update store-less grid
								row.data[column.field] = false;
							}
						}
					}, this);

					// Also update dirty data for rows that are not currently rendered
					for (id in this.dirty) {
						if (editedRow.id.toString() !== id && this.dirty[id][column.field]) {
							this.updateDirty(id, column.field, false);
						}
					}
				}
			}
		},

		_updateProperty: function (cellElement, oldValue, value, triggerEvent) {
			// Updates dirty hash and fires dgrid-datachange event for a changed value.
			var self = this;

			// test whether old and new values are inequal, with coercion (e.g. for Dates)
			if ((oldValue && oldValue.valueOf()) !== (value && value.valueOf())) {
				var cell = this.cell(cellElement);
				var row = cell.row;
				var column = cell.column;
				// Re-resolve cellElement in case the passed element was nested
				cellElement = cell.element;

				if (column.field && row) {
					var eventObject = {
						grid: this,
						cell: cell,
						oldValue: oldValue,
						value: value,
						bubbles: true,
						cancelable: true
					};
					if (triggerEvent && triggerEvent.type) {
						eventObject.parentType = triggerEvent.type;
					}

					if (on.emit(cellElement, 'dgrid-datachange', eventObject)) {
						if (this.updateDirty) {
							// for OnDemandGrid: update dirty data, and save if autoSave is true
							this.updateDirty(row.id, column.field, value);
							// perform auto-save (if applicable) in next tick to avoid
							// unintentional mishaps due to order of handler execution
							if (column.autoSave) {
								setTimeout(function () {
									self._trackError('save');
								}, 0);
							}
						}
						else {
							// update store-less grid
							row.data[column.field] = value;
						}
					}
					else {
						// Otherwise keep the value the same
						// For the sake of always-on editors, need to manually reset the value
						var cmp;
						if ((cmp = cellElement.widget)) {
							// set _dgridIgnoreChange to prevent an infinite loop in the
							// onChange handler and prevent dgrid-datachange from firing
							// a second time
							cmp._dgridIgnoreChange = true;
							cmp.set('value', oldValue);
							setTimeout(function () {
								cmp._dgridIgnoreChange = false;
							}, 0);
						}
						else if ((cmp = cellElement.input)) {
							this._updateInputValue(cmp, oldValue);
						}

						return oldValue;
					}
				}
			}
			return value;
		},

		_updateInputValue: function (input, value) {
			// summary:
			//		Updates the value of a standard input, updating the
			//		checked state if applicable.

			input.value = value;
			if (input.type === 'radio' || input.type === 'checkbox') {
				input.checked = input.defaultChecked = !!value;
			}
		},

		_retrieveEditorValue: function (column, cmp) {
			// summary:
			//		Intermediary between _convertEditorValue and
			//		_updatePropertyFromEditor.

			if (typeof cmp.get === 'function') { // widget
				return this._convertEditorValue(cmp.get('value'));
			}
			else { // HTML input
				return this._convertEditorValue(
					cmp[cmp.type === 'checkbox' || cmp.type === 'radio' ? 'checked' : 'value']);
			}
		},

		_convertEditorValue: function (value, oldValue) {
			// summary:
			//		Contains default logic for translating values from editors;
			//		tries to preserve type if possible.

			if (typeof oldValue === 'number') {
				value = isNaN(value) ? value : parseFloat(value);
			}
			else if (typeof oldValue === 'boolean') {
				value = value === 'true' ? true : value === 'false' ? false : value;
			}
			else if (oldValue instanceof Date) {
				var asDate = new Date(value);
				value = isNaN(asDate.getTime()) ? value : asDate;
			}
			return value;
		}
	});
});
