var Sort = function(table){
 	this.table = table; //hold Tabulator object
 	this.sortList = []; //holder current sort
 	this.changed = false; //has the sort changed since last render
 };

//initialize column header for sorting
Sort.prototype.initializeColumn = function(column, content){
	var self = this,
	sorter = false,
	colEl,
	arrowEl;


	switch(typeof column.definition.sorter){
		case "string":
		if(self.sorters[column.definition.sorter]){
			sorter = self.sorters[column.definition.sorter];
		}else{
			console.warn("Sort Error - No such sorter found: ", column.definition.sorter);
		}
		break;

		case "function":
		sorter = column.definition.sorter;
		break;
	}

	column.modules.sort = {
		sorter:sorter, dir:"none",
		params:column.definition.sorterParams || {},
		startingDir:column.definition.headerSortStartingDir || "asc",
		tristate: typeof column.definition.headerSortTristate !== "undefined" ? column.definition.headerSortTristate : this.table.options.headerSortTristate,
	};

	if(typeof column.definition.headerSort === "undefined" ? (this.table.options.headerSort !== false) : column.definition.headerSort !== false){

		colEl = column.getElement();

		colEl.classList.add("tabulator-sortable");


		arrowEl = document.createElement("div");
		arrowEl.classList.add("tabulator-col-sorter");

		if(typeof this.table.options.headerSortElement == "object"){
			arrowEl.appendChild(this.table.options.headerSortElement);
		}else{
			arrowEl.innerHTML = this.table.options.headerSortElement;
		}

		//create sorter arrow
		content.appendChild(arrowEl);

		column.modules.sort.element = arrowEl;

		//sort on click
		colEl.addEventListener("click", function(e){
			var dir = "",
			sorters=[],
			match = false;

			if(column.modules.sort){
				if(column.modules.sort.tristate){
					if(column.modules.sort.dir == "none"){
						dir = column.modules.sort.startingDir;
					}else{
						if(column.modules.sort.dir == column.modules.sort.startingDir){
							dir = column.modules.sort.dir == "asc" ? "desc" : "asc";
						}else{
							dir = "none";
						}
					}
				}else{
					switch(column.modules.sort.dir){
						case "asc":
						dir = "desc";
						break;

						case "desc":
						dir = "asc";
						break;

						default:
						dir = column.modules.sort.startingDir;
					}
				}


				if (self.table.options.columnHeaderSortMulti && (e.shiftKey || e.ctrlKey)) {
					sorters = self.getSort();

					match = sorters.findIndex(function(sorter){
						return sorter.field === column.getField();
					});

					if(match > -1){
						sorters[match].dir = dir;

						if(match != sorters.length -1){
							match = sorters.splice(match, 1)[0];
							if(dir != "none"){
								sorters.push(match);
							}
						}
					}else{
						if(dir != "none"){
							sorters.push({column:column, dir:dir});
						}
					}

					//add to existing sort
					self.setSort(sorters);
				}else{
					if(dir == "none"){
						self.clear();
					}else{
						//sort by column only
						self.setSort(column, dir);
					}

				}

				self.table.rowManager.sorterRefresh(!self.sortList.length);
			}
		});
	}
};

//check if the sorters have changed since last use
Sort.prototype.hasChanged = function(){
	var changed = this.changed;
	this.changed = false;
	return changed;
};

//return current sorters
Sort.prototype.getSort = function(){
	var self = this,
	sorters = [];

	self.sortList.forEach(function(item){
		if(item.column){
			sorters.push({column:item.column.getComponent(), field:item.column.getField(), dir:item.dir});
		}
	});

	return sorters;
};

//change sort list and trigger sort
Sort.prototype.setSort = function(sortList, dir){
	var self = this,
	newSortList = [];

	if(!Array.isArray(sortList)){
		sortList = [{column: sortList, dir:dir}];
	}

	sortList.forEach(function(item){
		var column;

		column = self.table.columnManager.findColumn(item.column);

		if(column){
			item.column = column;
			newSortList.push(item);
			self.changed = true;
		}else{
			console.warn("Sort Warning - Sort field does not exist and is being ignored: ", item.column);
		}

	});

	self.sortList = newSortList;

	if(this.table.options.persistence && this.table.modExists("persistence", true) && this.table.modules.persistence.config.sort){
		this.table.modules.persistence.save("sort");
	}
};

//clear sorters
Sort.prototype.clear = function(){
	this.setSort([]);
};

//find appropriate sorter for column
Sort.prototype.findSorter = function(column){
	var row = this.table.rowManager.activeRows[0],
	sorter = "string",
	field, value;

	if(row){
		row = row.getData();
		field = column.getField();

		if(field){

			value = column.getFieldValue(row);

			switch(typeof value){
				case "undefined":
				sorter = "string";
				break;

				case "boolean":
				sorter = "boolean";
				break;

				default:
				if(!isNaN(value) && value !== ""){
					sorter = "number";
				}else{
					if(value.match(/((^[0-9]+[a-z]+)|(^[a-z]+[0-9]+))+$/i)){
						sorter = "alphanum";
					}
				}
				break;
			}
		}
	}

	return this.sorters[sorter];
};

//work through sort list sorting data
Sort.prototype.sort = function(data){
	var self = this,
	sortList = this.table.options.sortOrderReverse ? self.sortList.slice().reverse() : self.sortList,
	sortListActual = [],
	rowComponents = [],
	lastSort;

	if(self.table.options.dataSorting){
		self.table.options.dataSorting.call(self.table, self.getSort());
	}

	self.clearColumnHeaders();

	if(!self.table.options.ajaxSorting){

		//build list of valid sorters and trigger column specific callbacks before sort begins
		sortList.forEach(function(item, i){
			var sortObj = item.column.modules.sort;

			if(item.column && sortObj){

				//if no sorter has been defined, take a guess
				if(!sortObj.sorter){
					sortObj.sorter = self.findSorter(item.column);
				}

				item.params = typeof sortObj.params === "function" ? sortObj.params(item.column.getComponent(), item.dir) : sortObj.params;

				sortListActual.push(item);
			}

			self.setColumnHeader(item.column, item.dir);
		});

		//sort data
		if (sortListActual.length) {
			self._sortItems(data, sortListActual);
		}

	}else{
		sortList.forEach(function(item, i){
			self.setColumnHeader(item.column, item.dir);
		});
	}

	if(self.table.options.dataSorted){
		data.forEach((row) => {
			rowComponents.push(row.getComponent());
		});

		self.table.options.dataSorted.call(self.table, self.getSort(), rowComponents);
	}

};

//clear sort arrows on columns
Sort.prototype.clearColumnHeaders = function(){
	this.table.columnManager.getRealColumns().forEach(function(column){
		if(column.modules.sort){
			column.modules.sort.dir = "none";
			column.getElement().setAttribute("aria-sort", "none");
		}
	});
};

//set the column header sort direction
Sort.prototype.setColumnHeader = function(column, dir){
	column.modules.sort.dir = dir;
	column.getElement().setAttribute("aria-sort", dir);
};

//sort each item in sort list
Sort.prototype._sortItems = function(data, sortList){
	var sorterCount = sortList.length - 1;

	data.sort((a, b) => {
		var result;

		for(var i = sorterCount; i>= 0; i--){
			let sortItem = sortList[i];

			result = this._sortRow(a, b, sortItem.column, sortItem.dir, sortItem.params);

			if(result !== 0){
				break;
			}
		}

		return result;
	});
};

//process individual rows for a sort function on active data
Sort.prototype._sortRow = function(a, b, column, dir, params){
	var el1Comp, el2Comp, colComp;

	//switch elements depending on search direction
	var el1 = dir == "asc" ? a : b;
	var el2 = dir == "asc" ? b : a;

	a = column.getFieldValue(el1.getData());
	b = column.getFieldValue(el2.getData());

	a = typeof a !== "undefined" ? a : "";
	b = typeof b !== "undefined" ? b : "";

	el1Comp = el1.getComponent();
	el2Comp = el2.getComponent();

	return column.modules.sort.sorter.call(this, a, b, el1Comp, el2Comp, column.getComponent(), dir, params);
};


//default data sorters
Sort.prototype.sorters = {

	//sort numbers
	number:function(a, b, aRow, bRow, column, dir, params){
		var alignEmptyValues = params.alignEmptyValues;
		var decimal = params.decimalSeparator;
		var thousand = params.thousandSeparator;
		var emptyAlign = 0;

		a = String(a);
		b = String(b);

		if(thousand){
			a = a.split(thousand).join("");
			b = b.split(thousand).join("");
		}

		if(decimal){
			a = a.split(decimal).join(".");
			b = b.split(decimal).join(".");
		}

		a = parseFloat(a);
		b = parseFloat(b);

		//handle non numeric values
		if(isNaN(a)){
			emptyAlign =  isNaN(b) ? 0 : -1;
		}else if(isNaN(b)){
			emptyAlign =  1;
		}else{
			//compare valid values
			return a - b;
		}

		//fix empty values in position
		if((alignEmptyValues === "top" && dir === "desc") || (alignEmptyValues === "bottom" && dir === "asc")){
			emptyAlign *= -1;
		}

		return emptyAlign;
	},

	//sort strings
	string:function(a, b, aRow, bRow, column, dir, params){
		var alignEmptyValues = params.alignEmptyValues;
		var emptyAlign = 0;
		var locale;

		//handle empty values
		if(!a){
			emptyAlign =  !b ? 0 : -1;
		}else if(!b){
			emptyAlign =  1;
		}else{
			//compare valid values
			switch(typeof params.locale){
				case "boolean":
				if(params.locale){
					locale = this.table.modules.localize.getLocale();
				}
				break;
				case "string":
				locale = params.locale;
				break;
			}

			return String(a).toLowerCase().localeCompare(String(b).toLowerCase(), locale);
		}

		//fix empty values in position
		if((alignEmptyValues === "top" && dir === "desc") || (alignEmptyValues === "bottom" && dir === "asc")){
			emptyAlign *= -1;
		}

		return emptyAlign;
	},

	//sort date
	date:function(a, b, aRow, bRow, column, dir, params){
		if(!params.format){
			params.format = "DD/MM/YYYY";
		}

		return this.sorters.datetime.call(this, a, b, aRow, bRow, column, dir, params);
	},

	//sort HH:mm formatted times
	time:function(a, b, aRow, bRow, column, dir, params){
		if(!params.format){
			params.format = "HH:mm";
		}

		return this.sorters.datetime.call(this, a, b, aRow, bRow, column, dir, params);
	},

	//sort datetime
	datetime:function(a, b, aRow, bRow, column, dir, params){
		var format = params.format || "DD/MM/YYYY HH:mm:ss",
		alignEmptyValues = params.alignEmptyValues,
		emptyAlign = 0;

		if(typeof moment != "undefined"){
			a = moment(a, format);
			b = moment(b, format);

			if(!a.isValid()){
				emptyAlign = !b.isValid() ? 0 : -1;
			}else if(!b.isValid()){
				emptyAlign =  1;
			}else{
				//compare valid values
				return a - b;
			}

			//fix empty values in position
			if((alignEmptyValues === "top" && dir === "desc") || (alignEmptyValues === "bottom" && dir === "asc")){
				emptyAlign *= -1;
			}

			return emptyAlign;

		}else{
			console.error("Sort Error - 'datetime' sorter is dependant on moment.js");
		}
	},

	//sort booleans
	boolean:function(a, b, aRow, bRow, column, dir, params){
		var el1 = a === true || a === "true" || a === "True" || a === 1 ? 1 : 0;
		var el2 = b === true || b === "true" || b === "True" || b === 1 ? 1 : 0;

		return el1 - el2;
	},

	//sort if element contains any data
	array:function(a, b, aRow, bRow, column, dir, params){
		var el1 = 0;
		var el2 = 0;
		var type = params.type || "length";
		var alignEmptyValues = params.alignEmptyValues;
		var emptyAlign = 0;

		function calc(value){

			switch(type){
				case "length":
				return value.length;
				break;

				case "sum":
				return value.reduce(function(c, d){
					return c + d;
				});
				break;

				case "max":
				return Math.max.apply(null, value) ;
				break;

				case "min":
				return Math.min.apply(null, value) ;
				break;

				case "avg":
				return value.reduce(function(c, d){
					return c + d;
				}) / value.length;
				break;
			}
		}

		//handle non array values
		if(!Array.isArray(a)){
			alignEmptyValues = !Array.isArray(b) ? 0 : -1;
		}else if(!Array.isArray(b)){
			alignEmptyValues = 1;
		}else{

			//compare valid values
			el1 = a ? calc(a) : 0;
			el2 = b ? calc(b) : 0;

			return el1 - el2;
		}

		//fix empty values in position
		if((alignEmptyValues === "top" && dir === "desc") || (alignEmptyValues === "bottom" && dir === "asc")){
			emptyAlign *= -1;
		}

		return emptyAlign;
	},


	//sort if element contains any data
	exists:function(a, b, aRow, bRow, column, dir, params){
		var el1 = typeof a == "undefined" ? 0 : 1;
		var el2 = typeof b == "undefined" ? 0 : 1;

		return el1 - el2;
	},

	//sort alpha numeric strings
	alphanum:function(as, bs, aRow, bRow, column, dir, params){
		var a, b, a1, b1, i= 0, L, rx = /(\d+)|(\D+)/g, rd = /\d/;
		var alignEmptyValues = params.alignEmptyValues;
		var emptyAlign = 0;

		//handle empty values
		if(!as && as!== 0){
			emptyAlign =  !bs && bs!== 0 ? 0 : -1;
		}else if(!bs && bs!== 0){
			emptyAlign =  1;
		}else{

			if(isFinite(as) && isFinite(bs)) return as - bs;
			a = String(as).toLowerCase();
			b = String(bs).toLowerCase();
			if(a === b) return 0;
			if(!(rd.test(a) && rd.test(b))) return a > b ? 1 : -1;
			a = a.match(rx);
			b = b.match(rx);
			L = a.length > b.length ? b.length : a.length;
			while(i < L){
				a1= a[i];
				b1= b[i++];
				if(a1 !== b1){
					if(isFinite(a1) && isFinite(b1)){
						if(a1.charAt(0) === "0") a1 = "." + a1;
						if(b1.charAt(0) === "0") b1 = "." + b1;
						return a1 - b1;
					}
					else return a1 > b1 ? 1 : -1;
				}
			}

			return a.length > b.length;
		}

		//fix empty values in position
		if((alignEmptyValues === "top" && dir === "desc") || (alignEmptyValues === "bottom" && dir === "asc")){
			emptyAlign *= -1;
		}

		return emptyAlign;
	},
};

Tabulator.prototype.registerModule("sort", Sort);
