/*
 * tasukeTabulatorManager.js
 * http://tasuke.dna.affrc.go.jp/
 *
 * Author: Jungsok Kim, Ryutaro Itoh, Norio Tabei
 *
 * Copyright (c) 2025 National Institute of Agrobiological Sciences
 * 
 * Required Modules:
 * js/jquery.js
 * js/jquery-ui.js
 * js/multiple-select
 * js/select2
 * js/tabulator.js
 * css/jquery-ui.css
 * css/multiple-select.css
 * css/tabulator.css
 * css/select2.css
 * css/font-awesome.css
 */
var TasukeTabulatorManager = function(divid, addColumns, tabledata, callbacks, opt={}) {
//	this.verbose = false;
	this.verbose = true;
	this.divid = divid;

	// Color info(static, not dynamic)
	this.colors = {
                rowHeaderBg: "#e6e6e6",
		rowHeaderFg: "#555",
		using: "black",
		usingIcon: "gray",
		notUsing: "#efb0a0",		// #ffa090
		notUsingIcon: "#efc0b0",	// #ffb0a0
		hit: "red",
		parentRow: "blue",
		childRow: "#8899ff"
	};

	var manager = this;	

	this.pushedKeyMap = {};
	document.addEventListener('keydown', function(e) {
//		event.preventDefault();

		if (e.repeat) return;
		if(manager.verbose) console.log("keydown:" + e.keyCode);
		manager.pushedKeyMap[e.keyCode] = true;
	});
	document.addEventListener('keyup', function(e) {
		if (e.repeat) return;
		if(manager.verbose) console.log("keyup:" + e.keyCode);
		delete manager.pushedKeyMap[e.keyCode];
	});

	// Private params.
	this.lastSelectedRows = [];
	this.maxUndoBuff = 10;
	this.undoBuff = [];
	this.redoBuff = [];
	this.lastUndoBuff = null;
	this.enableSorting = true;
	this.currentSelCond = true;
	this.select2id = '';
	this.select2SearchValAttr = 'searchval';
	this.defSelValue = true;
		if("defSelValue" in opt)  this.defSelValue = opt["defSelValue"];

	// Linked form IDs.
	this.buttonIdSel = divid + "_btn_sel";	// Private
	this.spanIdCurrentRows = "";
	this.spanIdCheckedRows = "";
	this.selectChangeUsing = "";
	this.buttonIdClearAllFilter = "";
	this.buttonIdUndo = "";
	this.buttonIdRedo = "";
	this.funcDisableBtnMoveRow = null;

	var filterPlaceholder = 'Filter';

	// System column definition
	var columns = [
		{title:"", field:"num", width:26, minWidth:20, hozAlign:"center", editor:false, sorter:"number", frozen:true, 
			formatter:function(cell){
				return '<span style="color:' + manager.colors['rowHeaderFg'] + ';font-size:smaller;">' + cell.getValue() + '</span>';
			}
/*
			headerFilter: function(cell, onRendered, success, cancel, editorParams) {
				var button = document.createElement("button");
				button.setAttribute("id", "tabulator_btn_allclear");
				button.setAttribute("title", "Clear all filters");
				button.className = 'icon-button';
				button.style.border = 'solid 1px gray';
				button.style.padding = '0px';
				button.style.marginTop = '3px';

				var icon = document.createElement("i");
				icon.setAttribute('aria-hidden', true);
				icon.className = 'fa fa-eraser';
				icon.style.fontSize = '12px';

				button.appendChild(icon);
				button.addEventListener("click", function(e) {
					manager.clearAllFilter();
				});

				return button;
			}
*/
		},
		{title:"", field:"sel", width:22, minWidth:22, hozAlign:"center", editor:false, formatter:"tickCross", sorter:"boolean", frozen:true,
//			headerFilter:"tickCross",  headerFilterParams:{"tristate":true}, headerFilterEmptyCheck:function(value){return value === null},
			headerFilter: function(cell, onRendered, success, cancel, editorParams) {
				var button = document.createElement("button");
				button.setAttribute("id", manager.buttonIdSel);
				button.setAttribute("title", "Check/Unckeck showing accessions");
				button.style.cursor = 'pointer';
				button.className = 'icon-button';
				button.style.padding = '0px';
				button.style.marginTop = '4px';

				var icon = document.createElement("i");
				icon.setAttribute('aria-hidden', true);
				icon.className = 'fa fa-check-square fa-lg';
//				icon.style.fontSize = '12px';

				button.appendChild(icon);
				button.addEventListener("click", function(e) {
					if(manager.currentSelCond === null || !manager.currentSelCond) {
						// "UnCheck" or "Check & Uncheck" -> Check all
						manager.checkRows('currentcheck');
					} else {
						// "Check" => Uncheck all
						manager.checkRows('currentclear');
					}
//					manager.setCheckedRows();
				});
				return button;
			},
			formatterParams: {
				tickElement:"<i class='fa fa-check-square fa-lg' aria-hidden='true' style='color:#0055ff;margin-top:1px;'></i>",
				crossElement: "<i class='fa fa-square-o fa-lg' aria-hidden='true' style='color:black;margin-top:1px;'></i>"
			},
			cellMouseDown: function(e, cell){
				// Fires before rowSelectionChanged
				manager.lastSelectedRows = manager.table.getSelectedRows();
			},
			cellClick: function(e, cell){
				// Fires after rowSelectionChanged
//				e.preventDefault();

				manager.addCheckHistory();
				
				var check = !cell.getValue();
				var row = cell.getRow();
				row.update({sel: check});
				if('18' in manager.pushedKeyMap) manager.checkChildRows(row, check);	// Alt

				var isRowSelected = false;
				var selectedRowIndexes = {};
				for(var i=0,len=manager.lastSelectedRows.length; i<len; i++) {
					selectedRowIndexes[manager.lastSelectedRows[i].getIndex()] = true;
					if(row === manager.lastSelectedRows[i]) isRowSelected = true;
				}
				var allRows = manager.table.getRows();
				var repRow = null;
				for(var i=0,len=allRows.length; i<len; i++) {
					if(allRows[i].getIndex() in selectedRowIndexes) {
						allRows[i].select();
						if(isRowSelected) {
							if(allRows[i].getData()['sel'] !== check) {
								allRows[i].update({sel: check});
								if(repRow === null) repRow = allRows[i];
							}
//							manager.checkChildRows(allRows[i], check);
						}
					} else {
						allRows[i].deselect();
					}
				}
				manager.syncFormAfterChecking();
				manager.oncheck(row.getData()["id"]);
			},
			cellDblClick: function(e, cell){
				return false;
			}
		},
		{title:"", field:"using", width:22, minWidth:22, hozAlign:"center", editor:false, formatter:"tickCross", sorter:"boolean", frozen:true,
			headerFilter:"tickCross",  headerFilterParams:{"tristate":true}, visible:false,
			headerFilterEmptyCheck:function(value){return value === null},
			formatterParams: {
				tickElement:"<i class='fa fa-eye fa-lg' aria-hidden='true' style='color:" + manager.colors.usingIcon + ";' title='Shown on track'></i>",
				crossElement: "<i class='fa fa-eye-slash fa-lg' aria-hidden='true' style='color:" + manager.colors.notUsingIcon + "' title='Hidden on track'></i>"
			}
		},
		{title:"ID", field:"id", width:120, editor:false, headerFilter:"input", headerFilterPlaceholder:filterPlaceholder, frozen:true,
			formatter:function(cell, formatterParams, onRendered) {
				var data = cell.getData();
				if(data.parentRow) {
					return '<div class="tabulator-data-tree-branch" style="margin-left: 9px;"></div>' + cell.getValue();
				} else {
					return cell.getValue();
				}
			}
		},
		{title:"", field:"sortorder", editor:false, sorter:"number", visible:false, frozen:true}	// Sorter for moveRow(Currently, moveRow does not update Position.)
	];

	// Data initialize + Determining column type(number/string)
	var tabledataType = {"variety":"number", "grp":"number", "origin":"number", "origin_2":"number", "type":"number", "other_1":"number", "other_2":"number"};
	var tabledataTypekeys = Object.keys(tabledataType);
	var reNotEmpty = /[\S]/;
	var reStrType = /[^\.\d\-\+E]/i;
	for(var j=0,len2=tabledataTypekeys.length; j<len2; j++) {
		var isEmpty = true;
		for(var i=0,len=tabledata.length; i<len; i++) {
			var val = tabledata[i][tabledataTypekeys[j]];
			if(reNotEmpty.test(val)) {
				isEmpty = false;
				if(reStrType.test(val)) {
					tabledataType[tabledataTypekeys[j]] = 'string';
					break;
				}
			}
		}
		if(isEmpty) tabledataType[tabledataTypekeys[j]] = 'string';
	}

	// Set Sorter & Filter.
	for(var i=0,len=addColumns.length; i<len ; i++) {
		var colDef = addColumns[i];
		if(colDef['field'] === 'sortorder') continue;
		if(colDef['field'] in tabledataType || colDef['sorter']==="number" || colDef['field']==='name') {
			if(tabledataType[colDef['field']] === "number" || colDef['sorter'] === "number") {
				colDef['sorter'] = "number";
				if(colDef["headerFilter"] !== false) {
					colDef['headerFilter'] = minMaxFilterEditor;
					colDef['headerFilterFunc'] = minMaxFilterFunction;
					colDef['headerFilterLiveFilter'] = false;
				}
			} else {
				colDef['sorter'] = "string";
				if(colDef["headerFilter"] !== false) {
					colDef['headerFilter'] = "input";
					colDef['headerFilterPlaceholder'] = filterPlaceholder;
				}
			}
		} else {
			if( !('sorter' in colDef) || colDef['sorter'] === "string") {
				colDef['sorter'] = "string";
				if(colDef["headerFilter"] !== false) {
					colDef['headerFilter'] = "input";
					colDef['headerFilterPlaceholder'] = filterPlaceholder;
				}
			} else if(colDef['sorter'] === "number") {
				//TODO Check operation.
				if(colDef["headerFilter"] !== false) {
					colDef['headerFilter'] = minMaxFilterEditor;
					colDef['headerFilterFunc'] = minMaxFilterFunction;
					colDef['headerFilterLiveFilter'] = false;
				}
			}
		}
		columns.push(colDef);
	}

	var movableRows = true;
	if("movableRows" in opt) movableRows = opt["movableRows"];

	// Build table.
	if(this.verbose) console.log("Start tabulator table initialize.");
	this.table = new Tabulator('#' + divid, {
		index : "id",
		height: 400,
		headerSortClickElement: "icon",		// Prohibit sorting by anything other than icon("sorting of all headers" prohibition is heavy, 
							// so an alternative is to make the icon invisible.)
		movableRows: movableRows,
		selectableRowsRangeMode: "click",
		selectableRows: true,
		tristate: true,
//		data: tabledata,
		'columns': columns,
		rowFormatter: function(row, data){
			//MEMO cell: row.getElement().childNodes[0].style.backgroundColor
			var hit = row.getData()['hit'];
			var color = "";
			if(hit) {
				color = manager.colors['hit'];
			} else {
				var using = row.getCell("using").getValue();
				if(!using) {
					color = manager.colors['notUsing'];
				} else {
					var data = row.getData();
					if('childRows' in data && data.childRows.length > 0) {
						color = manager.colors['parentRow'];
					} else if(data.parentRow) {
						color= manager.colors['childRow'];
					} else {
						color = manager.colors['using'];
					}
				}
			}
			row.getElement().style.color = color;
			row.getElement().childNodes[0].style.backgroundColor = manager.colors['rowHeaderBg'];	// Row number column.
//			row.getElement().childNodes[0].style.borderBottom = '1px solid #aaa'; 
		}
	});

	// Set Events
	this.table.on("tableBuilt", function(){
		if(manager.verbose) console.log("Start tableBuilt event.");
		manager.addData(tabledata);
		tabledata = null;	// Clear persisted scoped variables to free up memory. 

		// Hide sortorder column.
		var cols = manager.table.getColumns();
		for(var i=0,len=cols.length; i<len; i++) {
			var colDef = cols[i].getDefinition();
			if(colDef['field'] === 'sortorder') {
				cols[i].hide();
				break;
			}
		}

		// Add popup explanation to sort icon at column header.
		var sorters = $('.tabulator-arrow');
		for(var i=0,len=sorters.length; i<len ; i++) {
//			var colDef = cols[i].getDefinition();
			var titleStr = 'Sort';
//			if(colDef['title']) titleStr += ' by ' + colDef['title']
			$(sorters[i]).prop('title', titleStr);
		}
		setTimeout(function() {
			manager.syncFormAfterChecking();
//			manager.syncSelHeaderButton();
		}, 10);
		if('tableBuilt' in callbacks) callbacks['tableBuilt'](manager);
	});
	this.table.on("renderComplete", function(columns){
		if(manager.verbose) console.log("renderComplete");

		// Add popup explanation to Boolean filter.
		// It seems that its checkbox is recreated every time it is rendered.
		var chkboxs = $('.tabulator-header-filter input[type="checkbox"]');
		$(chkboxs[0]).prop('title', 'Filter by checking/unchecking');
		$(chkboxs[1]).prop('title', 'Filter by Shown/Hidden on  track');

		if('renderComplete' in callbacks) callbacks['renderComplete'](manager);
	});
//	this.table.on("rowMoving", function(row){ });
	this.table.on("rowMoved", function(row){
		var rows = manager.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var pos = rows[i]['_row']['position'];
			if(rows[i].getData()['sortorder'] !== pos) rows[i].update({sortorder:pos});
		}
//		manager.clearAllFilter();
		manager.table.setSort('sortorder', 'asc');
	});
//	this.table.on("dataSorted", function(sorters, rows){ });
	this.table.on("dataFiltered", function(filters, rows){
		if(manager.buttonIdClearAllFilter) {
			// If filtered, enable its clear button.
			var filtered = false;
			if(filters.length > 0) filtered = true;

			var button = $("#" + manager.buttonIdClearAllFilter);
			if(button) {
				if(filtered) {
					button.prop("disabled", false);
				} else {
					button.prop("disabled", true);
				}
			}
		}
		setTimeout(function() {
			manager.syncFormAfterChecking();
//			manager.syncSelHeaderButton();
		}, 10);
		if('dataFiltered' in callbacks) callbacks['dataFiltered'](manager, filters, rows);
	});
	this.table.on("rowClick", function(e, row) {
		if(manager.verbose) console.log("rowClick");
		if('rowClick' in callbacks) callbacks['rowClick'](manager, e, row);
	});
	this.table.on("dataLoading", function(data){
		if(manager.verbose) console.log("Start tabulator dataLoading.");
	});
	this.table.on("dataLoaded", function(data){
		if(manager.verbose) console.log("End tabulator dataLoading.");
	});
	this.table.on("dataProcessing", function(){
		if(manager.verbose) console.log("Start tabulator dataProcessing.");
	});
	this.table.on("dataProcessed", function(){
		if(manager.verbose) console.log("End tabulator dataProcessing.");
	});
	if("movableRows" in opt && opt["movableRows"]) this.table.on("rowMouseDown", function(e, row){ e.preventDefault(); });	// Block Cell Focus.
	this.table.on("rowSelectionChanged", function(data, rows, selected, deselected){
		manager.syncBtnMoveRow();
	});
}

// Static methods
TasukeTabulatorManager.getStandardColumns =  function() {
	return [
//		{title:"ID", field:"id", width:120, editor:false, headerFilter:"input", frozen:true},
		{title:"Name", field:"name", width:150, editor:false, headerFilter:"input"},
		{title:"Variety", field:"variety", width:64, editor:false, headerFilter:"input"},	//TODO The pull-down filter cannot be used unless the editor is enabled.
		{title:"Sub Variety", field:"grp", width:64, editor:false, headerFilter:"input"},
		{title:"Origin", field:"origin", width:120, editor:false},
		{title:"Origin2", field:"origin_2", width:120, editor:false},
		{title:"Type", field:"type", width:64, editor:false},
		{title:"Other1", field:"other_1", width:160, editor:false},
		{title:"Other2", field:"other_2", width:160, editor:false}
	];
}
TasukeTabulatorManager.getPhenotypeColumn = function(id, name) {
	return {
		title:name, field:id, width:110, editor:false, sorter:"number", visible:false
	};
}

// Instance methods
TasukeTabulatorManager.prototype = {
/*
	setDataset: function(columns, data=[]) {
		for(var i=0,len=columns.length; i<len; i++) {
			this.table.addColumn(columns[i]);
		}
	},
*/
	resetData: function(data) {
		this.table.clearData();
		this.addData(tabledata);
	},
	addData: function(data) {
		if(this.verbose) console.log("Start TasukeTabulatorManager.addData().");
		var initRow = function(onerow, num) {
			onerow.num = num;
			onerow.sel = this.defSelValue;
			onerow.using = true;
			onerow.hit = false;
			onerow.parentRow = null;
			onerow.childRows = [];
			num++;
			return num;
		};
		var dataClone = [];
		var num = 1;
		for(var i=0,len=data.length; i<len; i++) {
			var onerow = Object.assign({}, data[i]);	// Copy(not reference)
			num = initRow(onerow, num);
			dataClone.push(onerow);
		}
		this.table.addData(dataClone);
		if(this.verbose) console.log("End TasukeTabulatorManager.addData().");
	},
	changeChekedRow: function(chekedAccs = []) {
		var chekedAccsMap = {};
		for(var i=0,len=chekedAccs.length; i<len; i++) {
			chekedAccsMap[chekedAccs[i]] = true;
		}

		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			if(data.id in chekedAccsMap) {
				rows[i].update({'sel':true, 'using':true});
			} else {
				rows[i].update({'sel':false, 'using':false});
			}
		}
	},
	addCheckHistory: function() {
		if(this.lastUndoBuff) {
			var lenUndoBuff = this.undoBuff.length;
			if(lenUndoBuff >= this.maxUndoBuff) this.undoBuff.shift();
			this.undoBuff.push(this.lastUndoBuff);
		}
		this.redoBuff.splice(0);	// Empty array. 
		this.syncUndoButton();
	},
	undoCheckHistory: function() {
		var accMap = this.undoBuff.pop(); 
		if(!accMap) return;

		this.redoBuff.push(this.lastUndoBuff);
		this.applyCheckHistory(accMap);
	},
	redoCheckHistory: function() {
		var accMap = this.redoBuff.pop();
		if(!accMap) return;

		this.undoBuff.push(this.lastUndoBuff);
		this.applyCheckHistory(accMap);
	},
	applyCheckHistory: function(accMap) {
		//TODO Should use resetOrder()? But this proccess is very light.

		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var onerowData = rows[i].getData();
			var undoSel = accMap[onerowData['id']];
			if(onerowData['sel'] !== undoSel) {
				if(this.verbose) console.log("Undo:" + onerowData['id'] + "  " + onerowData['sel'] + " => " + undoSel);
				rows[i].update({sel: undoSel});
			}
		}
		this.syncFormAfterChecking();
//		this.setCheckedRows();
	},
	syncUndoButton: function() {
		if(this.buttonIdUndo) {
			var $btnUndo = $('#' + this.buttonIdUndo);
			if(this.undoBuff.length < 1) {
				$btnUndo.prop("disabled", true);
			} else {
				$btnUndo.prop("disabled", false);
			}
		}
		if(this.buttonIdRedo) {
			var $btnRedo = $('#' + this.buttonIdRedo);
			var $iconRedo = $('#' + this.buttonIdRedo + " i");
			if(this.redoBuff.length < 1) {
				$btnRedo.prop("disabled", true);
			} else {
				$btnRedo.prop("disabled", false);
			}
		}
	},
	getCurrentRowData: function(key = 'sel') {
		// This method returns the filtered rows in that order, so call clearAllFilter() beforehand if necessary.
		var rows = this.table.getRows();
		var dataMap = {};
		for(var i=0,len=rows.length; i<len; i++) {
			var pos = rows[i].getPosition(true);	// All row pos number.
//			if(pos < 0) continue;
			if(!pos) continue;

			var onerowData = rows[i].getData();
			if(onerowData[key]) dataMap[pos] = onerowData;
		}
		var posArr = Object.keys(dataMap);
		posArr.sort(function(a, b) {	// Sort by number
			return a - b;
		});

		var dataArr = [];
		for(var i=0, len=posArr.length; i<len ; i++) {
			dataArr.push(dataMap[posArr[i]]);
		}
		return dataArr;
	},
	getCurrentCheckedId: function(key = 'id') {
		// This method returns the filtered rows.
		var dataArr = this.getCurrentRowData('sel');
		var idArr = [];
		for(var i=0, len=dataArr.length; i<len; i++) {
			idArr.push(dataArr[i][key]);
		}
		return idArr;
	},
	getCurrentUsingId: function(key = 'id') {
		// This method returns the filtered rows.
		var dataArr = this.getCurrentRowData('using');
		var idArr = [];
		for(var i=0, len=dataArr.length; i<len; i++) {
			idArr.push(dataArr[i][key]);
		}
		return idArr;
	},
	setCurrentRows: function(rows) {
		if(!this.spanIdCurrentRows) return;
		$('#' + this.spanIdCurrentRows).text(rows.length);
//		$('#' + this.spanIdCurrentRows).text(rows.length + "/" + this.table.getRows().length);
	},
/*
	setCheckedRows: function() {
		if(!this.spanIdCheckedRows) return;

		var rows = this.table.getRows();
		var checkedRows = 0;
		for(var i=0,len=rows.length; i<len; i++) {
			if(rows[i].getData()['sel']) checkedRows++;
		}
		$('#' + this.spanIdCheckedRows).text(checkedRows);
	},
*/
	checkChildRows: function(row, check) {
		var data = row.getData();
		if('childRows' in data && data.childRows.length > 0 ) {
			for(var i=0,len=data.childRows.length; i<len; i++) {
				if(data.childRows[i].getData().sel !== check) {
					if(data.childRows[i].getData()['sel'] !== check) {
						data.childRows[i].update({'sel': check});
						data.childRows[i].reformat();
					}
				}
			}
		}
	},
	initActionSelector: function(id) {
		var actions = [
			['', '(Choose action)'],
			['currentcheck',	'Check:   Showing (Filtering)'],
			['currentclear',	'Uncheck: Showing (Filtering)'],
			['currenttoggle',	'Reverse: Showing (Filtering)'],
			['rowcheck',		'Check:   Selecting'],
			['rowclear',		'Uncheck: Selecting'],
			['rowtoggle',		'Reverse: Selecting'],
			['filteredclear',	'Uncheck: Hiding']
		];
		var $sel = $('#' + id);
		for(var i=0,len=actions.length; i<len; i++) {
			var opt = new Option(actions[i][1], actions[i][0], false, false);
			$sel.append(opt);
		}

		var manager = this;
		$sel.on('change', function(e) {
			manager.checkRows($(this).val());
			$(this).val("");
//			manager.setCheckedRows();
		});
	},
	initActionSelectorBtnList: function(id) {
		var $ul = $(document.createElement('ul'));
		$ul.addClass('ca_dropdown_lists');

                var actions = [
                        ['currentcheck',        '<b>Check</b>:   Showing (Filtering)'],
                        ['currentclear',        '<b>Uncheck</b>: Showing (Filtering)'],
                        ['currenttoggle',       '<b>Reverse</b>: Showing (Filtering)'],
                        ['rowcheck',            '<b>Check</b>:   Selecting'],
                        ['rowclear',            '<b>Uncheck</b>: Selecting'],
                        ['rowtoggle',           '<b>Reverse</b>: Selecting'],
                        ['filteredclear',       '<b>Uncheck</b>: Hiding']
                ];
		var manager = this;
		for(var i=0,len=actions.length; i<len; i++) {
			var $li = $(document.createElement('li'));
			$li.addClass('ca_dropdown_list');
			$btn = $('<button></button>', {
				value: actions[i][0],
				on: {
					'click': function() {
						manager.checkRows($(this).val());
//						manager.setCheckedRows();
					}
				}
			});
			$btn.append(actions[i][1]);
			$li.append($btn);
			$ul.append($li);
		}

		$('#' + id).append($ul);
		
	},
	initActionSelectorSimple: function(idBtnCheck, idBtnUncheck, idBtnToggle) {
		var actions = [
			[idBtnCheck, 'currentcheck', 'Check all'],
			[idBtnUncheck, 'currentclear', 'Uncheck all'],
			[idBtnToggle, 'currenttoggle', 'Reverse all']
		];

		var manager = this;
		for(var i=0,len=actions.length; i<len; i++) {
			var action = actions[i];
			if(!action) continue;

			var $btn = $('#' + action[0]);
			if(!$btn) continue;

			$btn.val(action[1]);
			$btn.append(action[2]);
			$btn.on('click', function() {
				manager.checkRows($(this).val());
//				manager.setCheckedRows();
			})
		}
	},
	initSelectColumnSelector: function(id) {
		var $sel = $('#' + id);

		var field2title = {num:'#Row number', using:'#"On track" symbol', id:'#ID'}
		var colDefs = this.table.getColumnDefinitions();
		for(var i=0,len=colDefs.length; i<len; i++) {
			var colDef = colDefs[i];
			if(colDef['field'] !== 'sel' && colDef['field'] !== 'sortorder') {
//			if(colDef['field'] !== 'num' && colDef['field'] !== 'sel' && colDef['field'] !== 'using' && colDef['field'] !== 'sortorder') {
				var selected = true;
				if('visible' in colDef && !colDef['visible']) selected = false;
				var title = colDef['title'];
				if(colDef['field'] in field2title) title = field2title[colDef['field']];
				var opt = new Option(title, colDef['field'], true, selected);
				$sel.append(opt);
			}
		}

		$sel.multipleSelect({
			width:120,
			formatSelectAll: function() {
				return '(All)';
			},
			formatAllSelected: function() {
				return '(All selected)';
			}
		});

		var manager = this;
		$sel.on('change', function(e) {
			var table = manager.table;
			var selectedColFields = {};
			$('#' + id + ' option:selected').each(function() {
				selectedColFields[$(this).val()] = true;
			});

			var cols = table.getColumns();
			for(var i=0,len=cols.length; i<len; i++) {
				var colDef = cols[i].getDefinition();
				if(colDef['field'] === 'sel') {
//				if(colDef['field'] === 'num' || colDef['field'] === 'sel'  || colDef['field'] === 'using') {
					cols[i].show();
				} else if(colDef['field'] in selectedColFields) {
					cols[i].show();
				} else {
					cols[i].hide();
				}
			}
		});
	},
	initSearchSelect2: function(id, parentId) {
		var manager = this;

		this.select2id = id;
		var $select2 = $('#' + id);
		$select2.select2({
			dropdownAutoWidth: true,
			width: 'auto',
			dropdownParent: $('#' + parentId),
			placeholder: "(Search by keyword)",
			closeOnSelect: false,
			matcher: function(params, data) {
				item = data.element.getAttribute(manager.select2SearchValAttr);
				return customMatcher(params, data, item);
			}
		});

		$select2.on('change', function(e) {
			var sel = $(this);
			var table = manager.table;
			var rows = table.getRows();
			for(var i=0,len=rows.length; i<len; i++) {
				if(!rows[i].getPosition(true)) continue;

				var data = rows[i].getData();
				if(data['id'] === sel.val()) {
					rows[i].update({'hit': true});
					rows[i].reformat();
					table.scrollToRow(rows[i], "center", false);
				} else {
					if(data.hit) {
						rows[i].update({'hit': false});
						rows[i].reformat();
					}
				}
			}
			sel.prop('selectedIndex', 0).trigger("change.select2");	// Reset to placeholder.
		});

//		this.syncList2SearchSelect2();	// Table rows are not available at initialization time.
	},
	initSelectChangeUsing: function(selId) {
		if(!selId) return;
		var $sel = $('#' + selId);
		if(!$sel) return;

		this.selectChangeUsing = selId;
		var modes = [
			["", "All", true],
			["1", "Only on track", false],
			["0", "Only hidden", false]
		];
		for(var i=0,len=modes.length; i<len; i++) {
			var opt = new Option(modes[i][1], modes[i][0], false, modes[i][2]);
			$sel.append(opt);
		}

		var manager = this;
		$sel.on('change', function() {
			var filterVal = $(this).val();
			var col = null;
			var cols = manager.table.getColumns();
			for(var i=0,len=cols.length; i<len; i++) {
				var colDef = cols[i].getDefinition();
					if(colDef['field'] === 'using') {
						col = cols[i];
						break;
					}
			}
			if(col) {
				if(filterVal === "1") {
					col.setHeaderFilterValue(true);
				} else if(filterVal === "0") {
					col.setHeaderFilterValue(false);
				} else {
					col.setHeaderFilterValue(null);	// clearFilter
				}
				col.reloadHeaderFilter();
			}
		});
		this.table.on("dataFiltered", function(filters, rows){
			if(manager.selectChangeUsing) {
				var val = "";
				for(var i=0,len=filters.length; i<len; i++) {
					if(filters[i]['field'] === 'using') {
						if(filters[i]['value']) {
							val = "1";
						} else {
							val = "0";
						}
						break;
					}
				}
				$('#' + manager.selectChangeUsing).val(val);
			}
		});
	},
	clearHistory: function() {
		this.clearSearchHit();
		this.undoBuff.splice(0);
		this.redoBuff.splice(0);
		this.lastUndoBuff = null;
	},
	clearSearchHit: function() {
		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			if(data['hit']) {
				rows[i].update({'hit': false});
				rows[i].reformat();
			}
		}
	},
	syncBtnMoveRow: function() {
/*
		if(this.verbose) {
			var caller = 'Unknown';
			if(this.syncBtnMoveRow.caller) caller = this.syncBtnMoveRow.caller.name;
			console.log("Call tasukeTabulatorManager.syncBtnMoveRow: from:" + caller);
		}
*/
		if(!this.funcDisableBtnMoveRow) return;

		var lenSelectedRows = this.table.getSelectedRows().length;
		if(lenSelectedRows === 0 || !this.enableSorting) {
			this.funcDisableBtnMoveRow(true);
		} else {
			this.funcDisableBtnMoveRow(false);
		}
	},
	syncList2SearchSelect2: function() {
		if(this.verbose) {
			var caller = 'Unknown';
			if(this.syncList2SearchSelect2.caller) caller = this.syncList2SearchSelect2.caller.name;
			console.log("Call tasukeTabulatorManager.syncList2SearchSelect2: " + id + "   from:" + caller);
			if(!caller) try{ throw new Error('Call tasukeTabulatorManager.syncList2SearchSelect2 Trace'); } catch(e) { console.log(e) };
		}
		var id = this.select2id;
		if(!id) return;

		var $select2 = $('#' + id);
		var rows = this.table.getRows();
		var searchAccArr = [];
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			searchAccArr.push(data);
//			var txt = data.id + "   " + data.name + "   " + data.variety;
//			searchAccArr.push({num:data.num, id:data.id, text:txt});
		}

		searchAccArr.sort(function(a, b) {
			return a.num - b.num;
		});

		var optEmpty = new Option("", "", false, false);
		$select2.find('option').remove().end().append(optEmpty);
//		$select2.find('option').remove().end().append(optEmpty).trigger('change');
		var keys = ['id','name','variety','grp','origin','origin_2','type','other_1','other_2'];
		for(var i=0,len=searchAccArr.length; i<len; i++) {
//			var opt = new Option(searchAccArr[i].text, searchAccArr[i].id, false, false);

			var data = searchAccArr[i];
			var opt = new Option( data.id + "   " + data.name + "   " + data.variety, data.id, false, false);		
			var searchvalArr = [];
			for(var j=0,len2=keys.length; j<len2 ; j++) {
				searchvalArr.push(data[keys[j]]);
			}
			// Add custom attribute to option tag.
			// It is automatically HTML escaped.
			opt.setAttribute(this.select2SearchValAttr, searchvalArr.join(';'));	// Add custom attribute to option tag.
			$select2.append(opt);
		}
		$select2.trigger('change');
	},
	syncFormAfterChecking: function() {
		// To avoid looping through the rows multiple times,
		//all form changes after the check operation are done here.

		// For: Number of checked rows.
		var checkedRows = 0;

		// For: Undo function.
		var lastUndoBuff = {};

		// For: Checking condition button on header.
		var $icon = $('#' + this.buttonIdSel + ' i');
		var btnChekingCond = null;
		var endBtnChekingCond = false;

		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			var pos = rows[i].getPosition(true);

			// For: Number of checked rows.
			if(data['sel']) checkedRows++;

			// For: Undo function.
			lastUndoBuff[data['id']] = data['sel'];

			// For: Checking condition button on header.
			if(!endBtnChekingCond && pos) {
				if(btnChekingCond === null) {
					btnChekingCond = data['sel'];
				} else if(btnChekingCond === data['sel']) {
					// Nothig to do.
				} else {
					btnChekingCond = null;
					endBtnChekingCond = true;
				}
			}
		}

		// For: Number of checked rows.
		if(this.spanIdCheckedRows) {
			$('#' + this.spanIdCheckedRows).text(checkedRows);
		}

		// For: Undo function.
		this.lastUndoBuff = lastUndoBuff;

		// For: Checking condition button on header.
		if($icon) {
			this.currentSelCond = btnChekingCond;
			if(this.currentSelCond === null) {
				// Checked and unchecked.
				$icon.addClass('fa-check-square-o');
				$icon.removeClass('fa-check-square');
				$icon.removeClass('fa-square-o');
				$icon.css('color', 'gray');
			} else if(this.currentSelCond) {
				// Checked only.
				$icon.addClass('fa-check-square');
				$icon.removeClass('fa-check-square-o');
				$icon.removeClass('fa-square-o');
				$icon.css('color', '#0075ff');
			}  else {
				// Unchecked only.
				 $icon.addClass('fa-square-o');
				$icon.removeClass('fa-check-square-o');
				$icon.removeClass('fa-check-square');
				$icon.css('color', 'black');
			}
		}

		this.syncUndoButton();
	},
/*
	syncSelHeaderButton: function() {
		var btnType = null;
		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			if(!rows[i].getPosition(true)) continue;

			var data = rows[i].getData();
			if(btnType === null) {
				btnType = data['sel'];
			} else if(btnType === data['sel']) {
				// Nothig to do.
			} else {
				btnType = null;
				break;
			}
		}

		var $icon = $("#" + this.buttonIdSel + ' i');
		if(!$icon) return;

		this.currentSelCond = btnType;
		if(this.currentSelCond === null) {
			// Checked and unchecked.
			$icon.addClass('fa-check-square-o');
			$icon.removeClass('fa-check-square');
			$icon.removeClass('fa-square-o');
			$icon.css('color', 'gray');
		} else if(this.currentSelCond) {
			// Checked only.
			$icon.addClass('fa-check-square');
			$icon.removeClass('fa-check-square-o');
			$icon.removeClass('fa-square-o');
			$icon.css('color', '#0075ff');
		}  else {
			// Unchecked only.
			$icon.addClass('fa-square-o');
			$icon.removeClass('fa-check-square-o');
			$icon.removeClass('fa-check-square');
			$icon.css('color', 'black');
		}
	},
*/
	checkRows: function(action) {
		if(!action) return;

		this.addCheckHistory();

		var table = this.table;
		var targetRows = [];
		if(/all/i.test(action)) {
			targetRows = table.getRows();
		} else if(/current/i.test(action)) {
			var allRows = table.getRows();
			for(var i=0,len=allRows.length; i<len; i++) {
				if(table.getRowPosition(allRows[i], true)) targetRows.push(allRows[i]);
			}
		} else if(/filtered/i.test(action)) {
			var allRows = table.getRows();
			for(var i=0,len=allRows.length; i<len; i++) {
				if(!table.getRowPosition(allRows[i], true)) targetRows.push(allRows[i]);
			}
		} else {
			targetRows = table.getSelectedRows();
		}

		var preChecked = null;	// Default: toggle
		if(/check/i.test(action)) {
			preChecked = true;
		} else if(/clear/i.test(action)) {
			preChecked = false;
		}

		for(var i=0,len=targetRows.length; i<len; i++) {
			var checked = preChecked;
			if(checked === null) checked = !targetRows[i].getCell("sel").getValue();
			if(targetRows[i].getData()['sel'] !== checked) targetRows[i].update({sel: checked});
		}
		this.oncheck(targetRows[0].getData()["id"]);
		this.syncFormAfterChecking();
//		this.syncSelHeaderButton();
//		this.setCheckedRows();
	},
	/*
		opt{order:bool, check:bool, clearSelect:bool}
	*/
	resetOrder: function(validIdOrder, opt = {order:true, check:true, using:true, clearSelect:true}) {
		if(this.verbose) {
			var caller = 'Unknown';
			if(this.resetOrder.caller) caller = this.resetOrder.caller.name;
			console.log("Call tasukeTabulatorManager.resetOrder(" + this.divid + "): " + JSON.stringify(opt) + "   from:" + caller);
			if(!caller) try{ throw new Error('Call tasukeTabulatorManager.resetOrder Trace'); } catch(e) { console.log(e) };
		}
		if(!opt) opt = {};
		if( !('order' in opt) ) opt['order'] = true;
		if( !('check' in opt) ) opt['check'] = true;
		if( !('using' in opt) ) opt['using'] = opt['check'];

		this.clearAllFilter();
		if(opt['clearSelect']) this.table.deselectRow();
		var rows = this.table.getRows();
		var id2rowMap = {};
		var invalidIdMap = {};
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			id2rowMap[data['id']] = rows[i];
			invalidIdMap[data['id']] = true;
		}

		var updateValidRow = function(n, id, parentRow, childRows, opt) {
			var data = {};
			if(opt['order']) data.num = n;
			data.parentRow = parentRow;
			data.childRows = childRows;
			if(opt['using']) data.using = true;
			if(opt['check']) data.sel = true;
			id2rowMap[id].update(data);
			id2rowMap[id].reformat();
			delete invalidIdMap[id];
		};
		var num = 1;
		for(var i=0,iLen=validIdOrder.length; i<iLen; i++) {
			var id = validIdOrder[i];
			if(id instanceof Array) {
				var dataParent = id2rowMap[validIdOrder[i][0]].getData();
				var numParent = num;
				num++;
				var childRows = [];
				for(var j=1,jLen=validIdOrder[i].length; j<jLen; j++) {
					updateValidRow(num, validIdOrder[i][j], id2rowMap[validIdOrder[i][0]], [], opt);
					childRows.push(id2rowMap[validIdOrder[i][j]]);
					num++;
				}
				updateValidRow(numParent, validIdOrder[i][0], null, childRows, opt);
			} else {
				updateValidRow(num, validIdOrder[i], null, [], opt);
				num++;
			}
		}
		
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();
			if(data['id'] in invalidIdMap) {
				var diffData = {'using':false, 'parentRow':null, 'childRows':[]};
				if(opt['order']) diffData['num'] = num;
				if(opt['check']) diffData['sel'] = false;
				rows[i].update(diffData);
				rows[i].reformat();
				num++;
			}
		}
		if(opt['order']) this.table.setSort("num", "asc");
		if(opt['check']) this.syncFormAfterChecking();
//		if(opt['check']) this.setCheckedRows();
	},
	changeCheck(idMap, defCheck=true) {
		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var data = rows[i].getData();

			var check = defCheck;
			if(!(data['id'] in idMap)) check = !check;
			if(data['sel'] !== check) {
				rows[i].update({sel:check});
				rows[i].reformat();
			}
		}
		this.syncFormAfterChecking();
	},
	clearAllFilter: function() {
		if(this.verbose) {
			var caller = 'Unknown';
			if(this.clearAllFilter.caller) caller = this.clearAllFilter.caller.name;
			console.log("Call tasukeTabulatorManager.clearAllFilter(" + this.divid + "):   from:" + caller);
			if(!caller) try{ throw new Error('Call tasukeTabulatorManager.clearAllFilter Trace'); } catch(e) { console.log(e) };
		}
/*
		var rows = this.table.getRows();
		var order = [];
		for(var i=0,len=rows.length; i<len; i++) {
			var pos = this.table.getRowPosition(rows[i]);
			order[pos] = rows[i].getData().id;
		}
*/
		this.table.clearHeaderFilter();
//		this.table.clearFilter(true);
	},
	enableAllColumnSorting: function(flg) {
		this.enableSorting = flg;
		if(flg) {
			$('.tabulator-arrow').css('display', 'block');
		} else {
			$('.tabulator-arrow').css('display', 'none');
		}
		this.syncBtnMoveRow();
/*
		// Very heavy!!
		var cols = this.table.getColumns();
		for(var i=0,len=cols.length; i<len; i++) {
			cols[i].updateDefinition({'headerSort': flg});
		}
*/
	},
/* I don't know how to disable MovableRows after initialization.
	enableMovableRows(flg) {
//		this.table.options.movableRows = flg;
//		this.table.modules.moveRow.registerTableOption("movableRows", flg);
//		this.table.optionsList.register('movableRows', flg);
console.log("enableMovableRows:" + this.table.options.movableRows);
//console.log(this.table.modules.moveRow);
delete this.table.modules.moveRow;
console.log(this.table.modules);
return;
		if(flg) {
			this.table.modules.moveRow.initialize();
		} else {
			this.table.modules.moveRow.connection = false;
			this.table.modules.moveRow.connectionSelectorsTables = false;
			this.table.modules.moveRow.connectionSelectorsElements = false;
		}
	},
*/
	moveRows: function(dir) {
		this.clearAllFilter();

		var posSorter = function(a, b) {
			return a['_row']['position'] - b['_row']['position'];
//			return a.getPosition(true) - b.getPosition(true);
		};


		var targetRows = this.table.getSelectedRows();
		if(!targetRows.length) return;
		targetRows.sort(posSorter);
		if(dir === 'down') targetRows.reverse();

		var allRows = this.table.getRows();
		allRows.sort(posSorter);
		var id2order = {};
		var order2row = {};
		for(var i=0,len=allRows.length; i<len; i++) {
			var id = allRows[i].getData()['id'];
			id2order[id] = i;
			order2row[i] = allRows[i];
		}

		for(var i=0,len=targetRows.length; i<len; i++) {
			var srcId = targetRows[i].getData()['id'];
			var srcOrder = id2order[srcId];
			var dstOrder = (dir === 'down') ? (srcOrder+1) : (srcOrder-1);
			if(!order2row[dstOrder] || order2row[dstOrder].isSelected()) continue;

			var srcRow = order2row[srcOrder];
			var dstRow = order2row[dstOrder];
			order2row[srcOrder] = dstRow;
			order2row[dstOrder] = srcRow;

			var dstId = dstRow.getData()['id'];
			id2order[srcId] = dstOrder;
			id2order[dstId] = srcOrder;
		}
		for(var i=0,len=allRows.length; i<len; i++) {
			var id = allRows[i].getData()['id'];
			allRows[i].update({sortorder:id2order[id]});
		}
		this.table.setSort('sortorder', 'asc');

/* The code below is simple, but it is heavy due to the many row.update() calls.
		var allRows = this.table.getRows();
		allRows.sort(posSorter);
		for(var i=0,len=allRows.length; i<len; i++) {
			allRows[i].update({sortorder:i});
		}

		var targetRows = this.table.getSelectedRows();
		if(!targetRows.length) return;
		targetRows.sort(posSorter);
		if(dir === 'down') targetRows.reverse();

		for(var i=0,len=targetRows.length; i<len; i++) {
			var srcOrder = targetRows[i].getData()['sortorder'];
			var dstOrder = (dir === 'down') ? (srcOrder+1) : (srcOrder-1);
			if(!allRows[dstOrder] || allRows[dstOrder].isSelected()) continue;
			var src = allRows[srcOrder];
			var dst = allRows[dstOrder];
			allRows[srcOrder] = dst;
			allRows[dstOrder] = src;
		}
		for(var i=0,len=allRows.length; i<len; i++) {
			if(allRows[i].getData()['sortorder'] !== id2order[id]) allRows[i].update({sortorder:i});
		}
		this.table.setSort('sortorder', 'asc');
*/
	},
	scrollToTop: function() {
		// console.log("call scrollToTop");
		var rows = this.table.getRows();
		for(var i=0,len=rows.length; i<len; i++) {
			var pos = rows[i].getPosition(true);
			if(pos === 1) {
				// console.log("scrollTo:" + rows[i].getData().name);
				this.table.scrollToRow(rows[i], "top", true);
				break;
			}
		}
	},
	oncheck: function(repId) {
		console.log("Call tasukeTabulatorManager.oncheck(dummy):" + repId);
	}
}

// Other Functions

var minMaxFilterEditor = function(cell, onRendered, success, cancel, editorParams){

    var end;

    var container = document.createElement("span");

    //create and style inputs
    var start = document.createElement("input");
    start.setAttribute("type", "number");
    start.setAttribute("placeholder", "Min");
//    start.setAttribute("min", 0);
//    start.setAttribute("max", 100);
    start.style.padding = "4px";
    start.style.marginRight = "2px";
    start.style.width = "49%";
    start.style.boxSizing = "border-box";

    start.value = cell.getValue();

    function buildValues(){
        success({
            start:start.value,
            end:end.value,
        });
    }

    function keypress(e){
        if(e.keyCode == 13){
            buildValues();
        }

        if(e.keyCode == 27){
            cancel();
        }
    }

    end = start.cloneNode();
    end.setAttribute("placeholder", "Max");
    end.style.marginRight = "0px";

    start.addEventListener("change", buildValues);
    start.addEventListener("blur", buildValues);
    start.addEventListener("keydown", keypress);

    end.addEventListener("change", buildValues);
    end.addEventListener("blur", buildValues);
    end.addEventListener("keydown", keypress);


    container.appendChild(start);
    container.appendChild(end);

    return container;
 }
//custom max min filter function
function minMaxFilterFunction(headerValue, rowValue, rowData, filterParams){
    //headerValue - the value of the header filter element
    //rowValue - the value of the column in this row
    //rowData - the data for the row being filtered
    //filterParams - params object passed to the headerFilterFuncParams property
	var val = parseFloat(rowValue);
	var start = parseFloat(headerValue.start);
	var end = parseFloat(headerValue.end);

        if(rowValue || val === 0){
            if(headerValue.start != ""){
                if(headerValue.end != ""){
                    return val >= start && val <= end;
                }else{
                    return val >= start;
                }
            }else{
                if(headerValue.end != ""){
                    return val <= end;
                }
            }
        }

    return true; //must return a boolean, true if it passes the filter.
}
function customMatcher(params, data, searchStrPre) {
	if ($.trim(params.term) === '') {
		return data;
	}

	if (typeof data.text === 'undefined') {
		return null;
	}
	var termLc = params.term.toLowerCase();
	var textLc = data.text.toLowerCase();
	var modifiedData;
	if (textLc.indexOf(termLc) > -1) {
		modifiedData = $.extend({}, data, true);
		return modifiedData;
	}

	if (searchStrPre === null || searchStrPre === void 0) {
		return null;
	}
	var textExtLc = searchStrPre.toString().toLowerCase();

	if (textExtLc.indexOf(termLc) > -1) {
		modifiedData = $.extend({}, data, true);
		return modifiedData;
	}
	return null;
};
