/*! RowReorder 1.2.0
 * 2015-2016 SpryMedia Ltd - datatables.net/license
 */

/**
 * @summary     RowReorder
 * @description Row reordering extension for DataTables
 * @version     1.2.0
 * @file        dataTables.rowReorder.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     www.sprymedia.co.uk/contact
 * @copyright   Copyright 2015-2016 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net
 */

(function (factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD
		define(['jquery', 'datatables.net'], function ($) {
			return factory($, window, document);
		});
	}
	else if (typeof exports === 'object') {
		// CommonJS
		module.exports = function (root, $) {
			if (!root) {
				root = window;
			}

			if (!$ || !$.fn.dataTable) {
				$ = require('datatables.net')(root, $).$;
			}

			return factory($, root, root.document);
		};
	}
	else {
		// Browser
		factory(jQuery, window, document);
	}
}(function ($, window, document, undefined) {
	'use strict';
	var DataTable = $.fn.dataTable;


	/**
	 * RowReorder provides the ability in DataTables to click and drag rows to
	 * reorder them. When a row is dropped the data for the rows effected will be
	 * updated to reflect the change. Normally this data point should also be the
	 * column being sorted upon in the DataTable but this does not need to be the
	 * case. RowReorder implements a "data swap" method - so the rows being
	 * reordered take the value of the data point from the row that used to occupy
	 * the row's new position.
	 *
	 * Initialisation is done by either:
	 *
	 * * `rowReorder` parameter in the DataTable initialisation object
	 * * `new $.fn.dataTable.RowReorder( table, opts )` after DataTables
	 *   initialisation.
	 * 
	 *  @class
	 *  @param {object} settings DataTables settings object for the host table
	 *  @param {object} [opts] Configuration options
	 *  @requires jQuery 1.7+
	 *  @requires DataTables 1.10.7+
	 */
	var RowReorder = function (dt, opts) {
		// Sanity check that we are using DataTables 1.10 or newer
		if (!DataTable.versionCheck || !DataTable.versionCheck('1.10.8')) {
			throw 'DataTables RowReorder requires DataTables 1.10.8 or newer';
		}

		// User and defaults configuration object
		this.c = $.extend(true, {},
			DataTable.defaults.rowReorder,
			RowReorder.defaults,
			opts
		);

		// Internal settings
		this.s = {
			/** @type {integer} Scroll body top cache */
			bodyTop: null,

			/** @type {DataTable.Api} DataTables' API instance */
			dt: new DataTable.Api(dt),

			/** @type {function} Data fetch function */
			getDataFn: DataTable.ext.oApi._fnGetObjectDataFn(this.c.dataSrc),

			/** @type {array} Pixel positions for row insertion calculation */
			middles: null,

			/** @type {Object} Cached dimension information for use in the mouse move event handler */
			scroll: {},

			/** @type {integer} Interval object used for smooth scrolling */
			scrollInterval: null,

			/** @type {function} Data set function */
			setDataFn: DataTable.ext.oApi._fnSetObjectDataFn(this.c.dataSrc),

			/** @type {Object} Mouse down information */
			start: {
				top: 0,
				left: 0,
				offsetTop: 0,
				offsetLeft: 0,
				nodes: []
			},

			/** @type {integer} Window height cached value */
			windowHeight: 0
		};

		// DOM items
		this.dom = {
			/** @type {jQuery} Cloned row being moved around */
			clone: null,

			/** @type {jQuery} DataTables scrolling container */
			dtScroll: $('div.dataTables_scrollBody', this.s.dt.table().container())
		};

		// Check if row reorder has already been initialised on this table
		var settings = this.s.dt.settings()[0];
		var exisiting = settings.rowreorder;
		if (exisiting) {
			return exisiting;
		}

		settings.rowreorder = this;
		this._constructor();
	};


	$.extend(RowReorder.prototype, {
		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Constructor
		 */

		/**
		 * Initialise the RowReorder instance
		 *
		 * @private
		 */
		_constructor: function () {
			var that = this;
			var dt = this.s.dt;
			var table = $(dt.table().node());

			// Need to be able to calculate the row positions relative to the table
			if (table.css('position') === 'static') {
				table.css('position', 'relative');
			}

			// listen for mouse down on the target column - we have to implement
			// this rather than using HTML5 drag and drop as drag and drop doesn't
			// appear to work on table rows at this time. Also mobile browsers are
			// not supported.
			// Use `table().container()` rather than just the table node for IE8 -
			// otherwise it only works once...
			$(dt.table().container()).on('mousedown.rowReorder touchstart.rowReorder', this.c.selector, function (e) {
				if (!that.c.enabled) {
					return;
				}

				var tr = $(this).closest('tr');

				// Double check that it is a DataTable row
				if (dt.row(tr).any()) {
					that._mouseDown(e, tr);
					return false;
				}
			});

			dt.on('destroy.rowReorder', function () {
				$(dt.table().container()).off('.rowReorder');
				dt.off('.rowReorder');
			});
		},


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Private methods
		 */

		/**
		 * Cache the measurements that RowReorder needs in the mouse move handler
		 * to attempt to speed things up, rather than reading from the DOM.
		 *
		 * @private
		 */
		_cachePositions: function () {
			var dt = this.s.dt;

			// Frustratingly, if we add `position:relative` to the tbody, the
			// position is still relatively to the parent. So we need to adjust
			// for that
			var headerHeight = $(dt.table().node()).find('thead').outerHeight();

			// Need to pass the nodes through jQuery to get them in document order,
			// not what DataTables thinks it is, since we have been altering the
			// order
			var nodes = $.unique(dt.rows({ page: 'current' }).nodes().toArray());
			var tops = $.map(nodes, function (node, i) {
				return $(node).position().top - headerHeight;
			});

			var middles = $.map(tops, function (top, i) {
				return tops.length < i - 1 ?
					(top + tops[i + 1]) / 2 :
					(top + top + $(dt.row(':last-child').node()).outerHeight()) / 2;
			});

			this.s.middles = middles;
			this.s.bodyTop = $(dt.table().body()).offset().top;
			this.s.windowHeight = $(window).height();
		},


		/**
		 * Clone a row so it can be floated around the screen
		 *
		 * @param  {jQuery} target Node to be cloned
		 * @private
		 */
		_clone: function (target) {
			var dt = this.s.dt;
			var clone = $(dt.table().node().cloneNode(false))
				.addClass('dt-rowReorder-float')
				.append('<tbody/>')
				.append(target.clone(false));

			// Match the table and column widths - read all sizes before setting
			// to reduce reflows
			var tableWidth = target.outerWidth();
			var tableHeight = 0;
			target.each(function (i, n) {
				tableHeight += $(n).outerHeight();
			});

			var sizes = target.children().map(function () {
				return $(this).width();
			});

			clone
				.width(tableWidth)
				.height(tableHeight)
				.find('tr').children().each(function (i) {
					this.style.width = sizes[i] + 'px';
				});

			// Insert into the document to have it floating around
			clone.appendTo('body');

			this.dom.clone = clone;
		},


		/**
		 * Update the cloned item's position in the document
		 *
		 * @param  {object} e Event giving the mouse's position
		 * @private
		 */
		_clonePosition: function (e) {
			var start = this.s.start;
			var topDiff = this._eventToPage(e, 'Y') - start.top;
			var leftDiff = this._eventToPage(e, 'X') - start.left;
			var snap = this.c.snapX;
			var left;

			if (snap === true) {
				left = start.offsetLeft;
			}
			else if (typeof snap === 'number') {
				left = start.offsetLeft + snap;
			}
			else {
				left = leftDiff + start.offsetLeft;
			}

			this.dom.clone.css({
				top: topDiff + start.offsetTop,
				left: left
			});
		},


		/**
		 * Emit an event on the DataTable for listeners
		 *
		 * @param  {string} name Event name
		 * @param  {array} args Event arguments
		 * @private
		 */
		_emitEvent: function (name, args) {
			this.s.dt.iterator('table', function (ctx, i) {
				$(ctx.nTable).triggerHandler(name + '.dt', args);
			});
		},


		/**
		 * Get pageX/Y position from an event, regardless of if it is a mouse or
		 * touch event.
		 *
		 * @param  {object} e Event
		 * @param  {string} pos X or Y (must be a capital)
		 * @private
		 */
		_eventToPage: function (e, pos) {
			if (e.type.indexOf('touch') !== -1) {
				return e.originalEvent.touches[0]['page' + pos];
			}

			return e['page' + pos];
		},


		/**
		 * Mouse down event handler. Read initial positions and add event handlers
		 * for the move.
		 *
		 * @param  {object} e      Mouse event
		 * @param  {jQuery} target TR element that is to be moved
		 * @private
		 */
		_mouseDown: function (e, target) {
			var that = this;
			var dt = this.s.dt;
			var start = this.s.start;
			dt.row($(target)).select();

			var offset = target.offset();
			start.top = this._eventToPage(e, 'Y');
			start.left = this._eventToPage(e, 'X');
			start.offsetTop = offset.top;
			start.offsetLeft = offset.left;
			start.nodes = $.unique(dt.rows({ page: 'current' }).nodes().toArray());

			// this will obtain rows in the their sorted order, regardless of which row is actually being dragged
			$.merge(target, target.siblings('.selected'));
			$.uniqueSort(target);
			//**********************************
			this._cachePositions();
			this._clone(target);
			this._clonePosition(e);

			// done after cloning so class doesn't get applied to it
			if (this.c.placeholder) {
				target.addClass(this.c.placeholder);
			}

			this.dom.target = target;
			target.addClass('dt-rowReorder-moving');

			$(document)
				.on('mouseup.rowReorder touchend.rowReorder', function (e) {
					that._mouseUp(e);
				})
				.on('mousemove.rowReorder touchmove.rowReorder', function (e) {
					that._mouseMove(e);
				});

			// Check if window is x-scrolling - if not, disable it for the duration
			// of the drag
			if ($(window).width() === $(document).width()) {
				$(document.body).addClass('dt-rowReorder-noOverflow');
			}

			// Cache scrolling information so mouse move doesn't need to read.
			// This assumes that the window and DT scroller will not change size
			// during an row drag, which I think is a fair assumption
			var scrollWrapper = this.dom.dtScroll;
			this.s.scroll = {
				windowHeight: $(window).height(),
				windowWidth: $(window).width(),
				dtTop: scrollWrapper.length ? scrollWrapper.offset().top : null,
				dtLeft: scrollWrapper.length ? scrollWrapper.offset().left : null,
				dtHeight: scrollWrapper.length ? scrollWrapper.outerHeight() : null,
				dtWidth: scrollWrapper.length ? scrollWrapper.outerWidth() : null
			};
		},


		/**
		 * Mouse move event handler - move the cloned row and shuffle the table's
		 * rows if required.
		 *
		 * @param  {object} e Mouse event
		 * @private
		 */
		_mouseMove: function (e) {
			this._clonePosition(e);

			// Transform the mouse position into a position in the table's body
			var bodyY = this._eventToPage(e, 'Y') - this.s.bodyTop;
			var middles = this.s.middles;
			var insertPoint = null;
			var dt = this.s.dt;
			var body = dt.table().body();
			var nodes = $.unique(dt.rows({ page: 'current' }).nodes().toArray());

			// Determine where the row should be inserted based on the mouse
			// position
			for (var i = 0, ien = middles.length; i < ien; i++) {
				if (bodyY < middles[i]) {
					insertPoint = i;
					break;
				}
			}

			if (insertPoint === null) {
				insertPoint = middles.length;
			}

			// Perform the DOM shuffle if it has changed from last time
			if (this.s.lastInsert === null || this.s.lastInsert !== insertPoint) {
				if (insertPoint === 0) {
					this.dom.target.prependTo(body);
				}
				else {
					if (insertPoint > this.s.lastInsert) {
						var buffer = this.dom.target.length - 1;

						if (!nodes[insertPoint + buffer]) {
							// reached end of table
							this.dom.target.appendTo(body);
						} else if ($(nodes[insertPoint]).hasClass('dt-rowReorder-moving')) {
							this.dom.target.insertBefore(nodes[insertPoint + buffer]);
						} else {
							this.dom.target.insertAfter(nodes[insertPoint + buffer - 1]);
						}
					}
					else {
						if ($(nodes[insertPoint]).hasClass('dt-rowReorder-moving')) {
							// do nothing
						} else {
							this.dom.target.insertBefore(nodes[insertPoint]);
						}
					}
				}

				this._cachePositions();

				this.s.lastInsert = insertPoint;
			}

			this._shiftScroll(e);
		},


		/**
		 * Mouse up event handler - release the event handlers and perform the
		 * table updates
		 *
		 * @param  {object} e Mouse event
		 * @private
		 */
		_mouseUp: function (e) {
			var that = this;
			var dt = this.s.dt;
			var i, ien;
			var dataSrc = this.c.dataSrc;

			this.dom.clone.remove();
			this.dom.clone = null;

			this.dom.target.removeClass('dt-rowReorder-moving');
			dt.rows($(this.dom.target)).deselect();
			if (this.c.placeholder) {
				this.dom.target.removeClass(this.c.placeholder);
			}
			//this.dom.target = null;

			$(document).off('.rowReorder');
			$(document.body).removeClass('dt-rowReorder-noOverflow');

			clearInterval(this.s.scrollInterval);
			this.s.scrollInterval = null;

			// Calculate the difference
			var startNodes = this.s.start.nodes;
			var endNodes = $.unique(dt.rows({ page: 'current' }).nodes().toArray());
			var idDiff = {};
			var fullDiff = [];
			var diffNodes = [];
			var getDataFn = this.s.getDataFn;
			var setDataFn = this.s.setDataFn;

			for (i = 0, ien = startNodes.length; i < ien; i++) {
				if (startNodes[i] !== endNodes[i]) {
					var id = dt.row(endNodes[i]).id();
					var endRowData = dt.row(endNodes[i]).data();
					var startRowData = dt.row(startNodes[i]).data();

					if (id) {
						idDiff[id] = getDataFn(startRowData);
					}

					fullDiff.push({
						node: endNodes[i],
						oldData: getDataFn(endRowData),
						newData: getDataFn(startRowData),
						newPosition: i,
						oldPosition: $.inArray(endNodes[i], startNodes)
					});

					diffNodes.push(endNodes[i]);
				}
			}

			// Create event args
			var eventArgs = [fullDiff, {
				dataSrc: dataSrc,
				nodes: diffNodes,
				values: idDiff,
				triggerRow: dt.rows(this.dom.target)
			}];

			// Emit event
			this._emitEvent('row-reorder', eventArgs);

			// Editor interface
			if (this.c.editor) {
				// Disable user interaction while Editor is submitting
				this.c.enabled = false;

				this.c.editor
					.edit(
						diffNodes,
						false,
						$.extend({ submit: 'changed' }, this.c.formOptions)
					)
					.multiSet(dataSrc, idDiff)
					.one('submitComplete', function () {
						that.c.enabled = true;
					})
					.submit();
			}

			// Do update if required
			if (this.c.update) {
				for (i = 0, ien = fullDiff.length; i < ien; i++) {
					var row = dt.row(fullDiff[i].node);
					var rowData = row.data();

					setDataFn(rowData, fullDiff[i].newData);

					// Invalidate the cell that has the same data source as the dataSrc
					dt.columns().every(function () {
						if (this.dataSrc() === dataSrc) {
							dt.cell(fullDiff[i].node, this.index()).invalidate('data');
						}
					});
				}

				// Trigger row reordered event
				this._emitEvent('row-reordered', eventArgs);

				dt.draw(false);
			}
		},


		/**
		 * Move the window and DataTables scrolling during a drag to scroll new
		 * content into view.
		 *
		 * This matches the `_shiftScroll` method used in AutoFill, but only
		 * horizontal scrolling is considered here.
		 *
		 * @param  {object} e Mouse move event object
		 * @private
		 */
		_shiftScroll: function (e) {
			var that = this;
			var dt = this.s.dt;
			var scroll = this.s.scroll;
			var runInterval = false;
			var scrollSpeed = 5;
			var buffer = 65;
			var
				windowY = e.pageY - document.body.scrollTop,
				windowVert,
				dtVert;

			// Window calculations - based on the mouse position in the window,
			// regardless of scrolling
			if (windowY < buffer) {
				windowVert = scrollSpeed * -1;
			}
			else if (windowY > scroll.windowHeight - buffer) {
				windowVert = scrollSpeed;
			}

			// DataTables scrolling calculations - based on the table's position in
			// the document and the mouse position on the page
			if (scroll.dtTop !== null && e.pageY < scroll.dtTop + buffer) {
				dtVert = scrollSpeed * -1;
			}
			else if (scroll.dtTop !== null && e.pageY > scroll.dtTop + scroll.dtHeight - buffer) {
				dtVert = scrollSpeed;
			}

			// This is where it gets interesting. We want to continue scrolling
			// without requiring a mouse move, so we need an interval to be
			// triggered. The interval should continue until it is no longer needed,
			// but it must also use the latest scroll commands (for example consider
			// that the mouse might move from scrolling up to scrolling left, all
			// with the same interval running. We use the `scroll` object to "pass"
			// this information to the interval. Can't use local variables as they
			// wouldn't be the ones that are used by an already existing interval!
			if (windowVert || dtVert) {
				scroll.windowVert = windowVert;
				scroll.dtVert = dtVert;
				runInterval = true;
			}
			else if (this.s.scrollInterval) {
				// Don't need to scroll - remove any existing timer
				clearInterval(this.s.scrollInterval);
				this.s.scrollInterval = null;
			}

			// If we need to run the interval to scroll and there is no existing
			// interval (if there is an existing one, it will continue to run)
			if (!this.s.scrollInterval && runInterval) {
				this.s.scrollInterval = setInterval(function () {
					// Don't need to worry about setting scroll <0 or beyond the
					// scroll bound as the browser will just reject that.
					if (scroll.windowVert) {
						document.body.scrollTop += scroll.windowVert;
					}

					// DataTables scrolling
					if (scroll.dtVert) {
						var scroller = that.dom.dtScroll[0];

						if (scroll.dtVert) {
							scroller.scrollTop += scroll.dtVert;
						}
					}
				}, 20);
			}
		}
	});



	/**
	 * RowReorder default settings for initialisation
	 *
	 * @namespace
	 * @name RowReorder.defaults
	 * @static
	 */
	RowReorder.defaults = {
		/**
		 * Data point in the host row's data source object for where to get and set
		 * the data to reorder. This will normally also be the sorting column.
		 *
		 * @type {Number}
		 */
		dataSrc: 0,

		/**
		 * Editor instance that will be used to perform the update
		 *
		 * @type {DataTable.Editor}
		 */
		editor: null,

		/**
		 * Enable / disable RowReorder's user interaction
		 * @type {Boolean}
		 */
		enabled: true,

		/**
		 * Form options to pass to Editor when submitting a change in the row order.
		 * See the Editor `from-options` object for details of the options
		 * available.
		 * @type {Object}
		 */
		formOptions: {},

		/**
		 * Drag handle selector. This defines the element that when dragged will
		 * reorder a row.
		 *
		 * @type {String}
		 */
		selector: 'td:first-child',

		/**
		 * Optionally lock the dragged row's x-position. This can be `true` to
		 * fix the position match the host table's, `false` to allow free movement
		 * of the row, or a number to define an offset from the host table.
		 *
		 * @type {Boolean|number}
		 */
		snapX: false,

		/**
		 * Update the table's data on drop
		 *
		 * @type {Boolean}
		 */
		update: true,

		/**
		* This mimics jQuery UI sortable placeholder. Provide a class to be applied to tr.
		* 
		* @type {String}
		*/
		placeholder: false
	};


	/*
	 * API
	 */
	var Api = $.fn.dataTable.Api;

	// Doesn't do anything - work around for a bug in DT... Not documented
	Api.register('rowReorder()', function () {
		return this;
	});

	Api.register('rowReorder.enable()', function (toggle) {
		if (toggle === undefined) {
			toggle = true;
		}

		return this.iterator('table', function (ctx) {
			if (ctx.rowreorder) {
				ctx.rowreorder.c.enabled = toggle;
			}
		});
	});

	Api.register('rowReorder.disable()', function () {
		return this.iterator('table', function (ctx) {
			if (ctx.rowreorder) {
				ctx.rowreorder.c.enabled = false;
			}
		});
	});


	/**
	 * Version information
	 *
	 * @name RowReorder.version
	 * @static
	 */
	RowReorder.version = '1.2.0';


	$.fn.dataTable.RowReorder = RowReorder;
	$.fn.DataTable.RowReorder = RowReorder;

	// Attach a listener to the document which listens for DataTables initialisation
	// events so we can automatically initialise
	$(document).on('init.dt.dtr', function (e, settings, json) {
		if (e.namespace !== 'dt') {
			return;
		}

		var init = settings.oInit.rowReorder;
		var defaults = DataTable.defaults.rowReorder;

		if (init || defaults) {
			var opts = $.extend({}, init, defaults);

			if (init !== false) {
				new RowReorder(settings, opts);
			}
		}
	});


	return RowReorder;
}));