js/trans.js

/**
 * @file trans.js The core class of Translator++
 * @author Dreamsavior 
 * File version: 2024-04-03 14:22:34.304
 */


/**
 * Executed each time trans file is loaded or initialized
 * @event Trans#transLoaded
 */

window.fs 		= require('graceful-fs');
window.afs 		= require('await-fs');
window.nwPath 	= require('path');
window.spawn 	= require('child_process').spawn;
window.debounce = require("debounce");


//================================================================
//
// COMMON FUNCTION
//
//================================================================
/**
 * Insert an array into another array at some index
 * @global
 * @param {Array} array - Source array
 * @param {Number} index - Index to insert at
 * @param {Array} arrayToInsert - Array to insert
 * @returns {Array} - Merged array
 */
window.insertArrayAt = function(array, index, arrayToInsert) {
    Array.prototype.splice.apply(array, [index, 0].concat(arrayToInsert));
    return array;
}

global.common ||= {}

common.arrayExchange = function(arr, fromIndex, toIndex) {
	// exchange an array value by index (single)
	var element = arr[fromIndex];
	arr.splice(fromIndex, 1);
	arr.splice(toIndex, 0, element);
}

common.arrayExchangeBatch = function(input, fromIndex, toIndex) {
	// exchange an array value by indexes(array)
	if (Array.isArray(fromIndex) == false) {
		common.arrayExchange(input, fromIndex, toIndex);
	}
	for (var i=fromIndex.length-1; i>=0; i--) {
		common.arrayExchange(input, fromIndex[i], toIndex);
	}
	return input;
}


common.arrayMove = function(array, fromIndex, to) {
	// move an array index to new index
	if(Array.isArray(array) == false) return array;
	if( to === fromIndex ) return array;

	var target = array[fromIndex];                         
	var increment = to < fromIndex ? -1 : 1;

	for(var k = fromIndex; k != to; k += increment){
	array[k] = array[k + increment];
	}
	array[to] = target;
	return array;	
}

common.arrayMoveBatch = function(array, fromIndex, to) {
	if (Array.isArray(fromIndex) == false) {
		return common.arrayMove(array, fromIndex, to);
	}
	
	var n=0;
	for (var i=fromIndex.length-1; i>=0; i--) {
		array = common.arrayMove(array, fromIndex[i]+n, to);
		n++;
	}
	return array;
}


common.escapeSelector = function(string) {
	string = string||"";
	if (typeof string !== 'string') return false;
    //return string.replace( /(:|\.|\[|\]|,|=|@)/g, "\\$1" );
    //return string.replace( /(:|\.|\[|\]|,|=|@|\s|\(|\))/g, "\\$1" );
	//return '"'+string+'"';
	return ('"'+CSS.escape(string)+'"')
}

common.arrayInsert =function(thisArray, index, item ) {
    thisArray.splice( index, 0, item );
	return thisArray;
};

common.batchArrayInsert = function(thisArray, index, item ) {
	for (var i=0; i<thisArray.length; i++) {
		this.arrayInsert(thisArray[i], index, item);
	}
	return thisArray;
};

var FileLoader = function() {
	this.handler = {};
}

FileLoader.prototype.add = function(extension, handler) {
	// handler is function with arguments : filepath
	this.handler[extension] = handler;
}
FileLoader.prototype.open = function(extension, handler) {
	// handler is function with arguments : filepath
	//this.handler['extension'] = handler;
}

window.FileLoader = FileLoader;


//================================================================
//
// 					T R A N S   C L A S S
//
//================================================================

/**
 * @class
 * @classdesc
 * The core class of Translator++
 * Handle basic logic of Translator++ application
 * This class will have one instance for each window. Which is `window.trans`
 */
class Trans extends require('www/js/BasicEventHandler.js') {
	constructor() {
		super($(document));
		this.init();
	}
}

/**
 * Maximum column allowed in the grid
 */
Trans.maxCols = 15;


/**
 * Handle cell level information
 * @class
 * @since 4.4.4
 * @classdesc
 * Manage cell's additional informations
 */
Trans.CellInfo = function() {
	
}

/**
 * Get all cell information
 * @param {String} file - The file ID
 * @returns {Array} - The cell information
 */
Trans.CellInfo.prototype.getAll = function(file) {
	if (!file) return;
	if (!trans?.project?.files?.[file]) return;
	var obj = trans.getObjectById(file);
	if (!obj.cellInfo) obj.cellInfo = [];
	return obj.cellInfo;
}

/**
 * Get row information
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @returns {Array} - The row information
 */
Trans.CellInfo.prototype.getRow = function(file, row) {
	var cellInf = this.getAll(file);
	if (!cellInf) return;
	return cellInf[row];
}

/**
 * Get cell information
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @param {Number} col - The column number
 * @returns {Object} - The cell information
 */
Trans.CellInfo.prototype.getCell = function(file, row, col) {
	var rowInf = this.getRow(file, row);
	if (!rowInf) return;
	return rowInf[col];
}

/**
 * Get the best translation cell information
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @param {String} key - The key to get
 * @returns {Object} - The cell information
 */
Trans.CellInfo.prototype.getBestCellInfo = function(file, row, key) {
	var data 	= trans.getData(file);
	var col 	= trans.getTranslationColFromRow(data[row]);
	var cellInfo = this.getCell(file, row, col);
	if (!cellInfo) return cellInfo;
	if (key) return cellInfo[key];
	return cellInfo;
}

/**
 * Get configuration of a cell
 * @param {String} key - The key to get
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @param {Number} col - The column number
 * @returns {*} - The value of the key
 */
Trans.CellInfo.prototype.get = function(key, file, row, col) {
	var cellInf = this.getCell(file, row, col);
	if (!cellInf) return;
	return cellInf[key];
}

/**
 * Set cell information
 * @param {String} key - The key to set
 * @param {*} value - The value to set
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @param {Number} col - The column number
 * @returns {Boolean} - True if success
 */
Trans.CellInfo.prototype.set = function(key, value, file, row, col) {
	//console.log("Setting cell info with options:", arguments);
	var cellInf = this.getAll(file);
	cellInf[row] =  cellInf[row] || [];
	cellInf[row][col] = cellInf[row][col] || {};
	cellInf[row][col][key] = value;
	return true;
}

/**
 * Delete cell information
 * @param {String} key - The key to delete
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @param {Number} col - The column number
 * @returns {Boolean} - True if success
 */
Trans.CellInfo.prototype.delete = function(key, file, row, col) {
	//console.log("Setting cell info with options:", arguments);
	var cellInf = this.getAll(file);
	cellInf[row] =  cellInf[row] || [];
	cellInf[row][col] = cellInf[row][col] || {};
	delete cellInf[row][col][key]
	return true;
}

/**
 * Delete row information
 * @param {String} file - The file ID
 * @param {Number} row - The row number
 * @returns {Boolean} - True if success
 */
Trans.CellInfo.prototype.deleteRow = function(file, row) {
	var cellInf = this.getAll(file)
	if (empty(cellInf)) return;
	if (Array.isArray(cellInf)) {
		cellInf.splice(row, 1);
	} else {
		delete cellInf[row];
	}
}

// Trans.CellInfo.prototype.moveColumn = function(file, from, to) {
// 	var cellInf = this.getAll(file)
// 	if (empty(cellInf)) return;
// 	if (Array.isArray(cellInf)) {
// 		cellInf.splice(row, 1);
// 	} else {
// 		delete cellInf[row];
// 	}
// 	return true;
// }

Trans.CellInfo.prototype.deleteCell = function(file, rowId, cellId) {
	var thisRow = this.getRow(file, rowId);
	if (!Array.isArray(thisRow)) return;
	thisRow.splice(cellId, 1);
	return true;
}

Trans.CellInfo.prototype.moveCell = function(file, rowId, from, to) {
	var thisRow = this.getRow(file, rowId);
	if (!Array.isArray(thisRow)) return;
	common.arrayMoveBatch(thisRow, from, to);
	return true;
}



/**
 * Create a new event with JQuery eventing convenience
 * Equal to `$(document).on()`
 * @param {String} evt - Event name
 * @param {Function} fn - Function to trigger
 * @since 4.3.20
 * trans.on('transLoaded', (e, opt)=> {
 * 	// do something
 * })
 */


/**
 * Removes an event
 * Equal to `$(document).off()`
 * @param {String} evt - Event name
 * @param {Function} fn - Function to trigger
 * @since 4.3.20
 * @example
 * trans.off('transLoaded', (e, opt)=> {
 * 	// do something
 * })
 */


/**
 * Run the event once
 * Trigger an event and immediately removes it
 * Equal to `$(document).one()`
 * @param {String} evt - Event name
 * @param {Function} fn - Function to trigger
 * @since 4.3.20
 */


/**
 * Trigger an event
 * Equal to `$(document).trigger()`
 * @param {String} evt - Event name
 * @param {Function} fn - Function to trigger
 * @since 4.3.20
 */


/**
 * Initialization of the Trans object
 */
Trans.prototype.init = function() {
	this.config ={
		loadRomaji		:true,
		maxRequestLength:3000,
		autoSaveEvery	:600,
		batchDelay		:5000,
		rpgTransFormat	:true,
		autoTranslate	:false
	},
	/**
	 * Index of key column. The column to store original texts.
	 * @default 0
	 */
	this.keyColumn 			= 0;
	this.isFreeEditing		= false;
	this.gameTitle			=""
	this.gameEngine			=""
	this.projectId			=""
	this.indexIds			={}
	this.fileListLoaded		=false
	this.isLoadingFileList	=false
	this.unsavedChange		=false
	//files:{},
	this.gameFolder			= ''
	this.currentFile		= '' //current .trans file
	this.skipElement		=['note', 'Comment', 'Script']
	/**
	 * The current project
	 */
	this.project			=undefined
	this.timers				={}
	/**
	 * The current active data on the grid
	 */
	this.data 				=[];
	this.colHeaders			=[t('Original Text'), t('Initial'), t('Machine translation'), t('Better translation'), t('Best translation')];
	
	
	this.onFileNavLoaded	= function() {}
	this.onFileNavUnloaded	= function() {}
	
	this.validateKey = function(value, callback) {
		console.log("key validator", value);
		if (value=='' || value==null) {
			callback(false);
		} else {
			callback(true);
		}
	}
	
	this.columns = [{
			readOnly: false,
			validator: this.validateKey,
			width: 150,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		}
	];

	this.default = {};
	this.default.columns = [{
			readOnly: false,
			validator: this.validateKey,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		},
		{
			readOnly: false,
			//trimWhitespace: false
		}

	]
	/**
	 * Whether trans is currently handling a project or not.
	 */
	this.inProject = false; 
	this.default.colHeaders	=[t('Original Text'), t('Initial'), t('Machine translation'), t('Better translation'), t('Best translation')];
}

Trans.prototype.isTrans = function(obj) {
	if (!obj) return false;
	if (obj.constructor.name!=="Object" && obj.constructor.name!=="Trans") return false;
	if (!obj?.project?.files) return false;
	return true;
}

/**
 * Get project's option
 * Option are user editable configuration
 * @param  {String} key
 */
Trans.prototype.getOption = function(key) {
	if (!trans.project) return;
	trans.project.options ||= {};

	// override default behavior
	if (typeof trans.project.options.gridInfo == "undefined") {
		trans.project.options.gridInfo = {
			isRuleActive	: true,
			enableTrail		: true,
			viewTrail		: false,
			viewOrganicCellMarker:true,
			rowHeaderInfo	: false
		}
	}


	return trans.project.options[key];
}

Trans.prototype.setOption = function(key, value) {
	if (!trans.project) return;
	trans.project.options ||= {};
	trans.project.options[key] = value;
}

/**
 * Get project configuration
 * Config are system defined configuration. Should not editable by user.
 * @param  {String} key - The configuration key
 * @returns {*} - The configuration related to the Key
 */
Trans.prototype.getConfig = function(key) {
	trans.project.config = trans.project.config || {};
	if (typeof key == "string") return trans.project.config[key];
	if (Array.isArray(key)) {
		var result = trans.project.config;
		try {
			for (var i=0; i<key.length; i++) {
				result = result[key[i]];
			}
			return result;
		} catch (e) {
			return;
		}
	}
	return;
}

/**
 * Set key-value pair of configuration
 * @param  {(String|String[])} key - The key
 * @param  {*} value - The value
 * @returns {Boolean} - True if success
 */
Trans.prototype.setConfig = function(key, value) {
	trans.project.config = trans.project.config || {};
	if (typeof key == "string") {
		trans.project.config[key] = value;
		return true;
	}
	
	if (Array.isArray(key)) {
		var result = trans.project.config;
		try {
			for (var i=0; i<key.length-1; i++) {
				result[key[i]] = result[key[i]] || {};
				result = result[key[i]];
			}
			result[key[i]] = value;
			return true;
		} catch (e) {
			return;
		}
	}
	return;	
}

/**
 * Check if a project is currently opened
 * @returns {Boolean} - Returns true if trans is in a project
 * @since 6.1.18
 */
Trans.prototype.isInProject = function() {
	return this.inProject;
}

/**
 * Get the template path
 * @returns {String} - The path to the template file
 */
Trans.prototype.getTemplatePath = function() {
	var templatePath = sys.config.templatePath||nw.App.manifest.localConfig.defaultTemplate
	
	fs = fs||require('fs')	
	try {
		if (fs.existsSync(templatePath)) {
			return templatePath;
		}
	} catch (err) {
		return nwPath.join(__dirname, templatePath);
	}
}

/**
 * merge reference into files object in transObj.project
 * @function
 * @param  {object} transObj instance of trans object
 * @returns {object} instance of trans object
 */
Trans.prototype.mergeReference = function(transObj) {
	console.log("Merging reference");
	transObj = transObj||{};
	transObj.project = transObj.project||{};
	transObj.project.references = transObj.project.references||{};
	transObj.project.files = transObj.project.files||{};
	for (let ref in transObj.project.references) {
		console.log("assigning : ", ref);
		transObj.project.files[ref] = this.project.references[ref];
	}
	
	return transObj;
	
}
/**
 * determine wether the pathname is supported file formats
 * @function
 * @param  {string} pathName
 * @returns {boolean} True if file is supported, otherwise false.
 */
Trans.prototype.isFileSupported = function(pathName) {
	if (typeof pathName !== 'string') return false;
	var ext = common.getFileExtension(pathName);
	if (sys.supportedExtension.includes(ext)) return true;
	return false;
}

/**
 * do some action depending on the file type
 * @function
 * @param  {string} file path to the file
 */
Trans.prototype.openFile = function(file) {
	const spawn = require("child_process").spawn;
	if(this.isFileSupported(file) == false) return false;
	var ext = common.getFileExtension(file);
	if (typeof (this.fileLoader.handler[ext]) !== 'function') return false;
	if (this.fileListLoaded == false) {
		// load in this window
		ui.introWindowClose();
		this.fileLoader.handler[ext].apply(this, [file]);
	} else {
		// load on new window
		//var spawn = spawn || require('child_process').spawn;
		spawn(nw.process.execPath, [file], {
				detached :true
			});
	}
}

/**
 * Initialize the project
 * @function
 * @todo Implement this function
 */
Trans.prototype.initProject = function() {
	if (typeof this.project !== 'undefined') {
		console.log("project is already been initialized! skipping!");
		return false
	}	
	// this function is not done yet
}

/**
 * Close the current project
 * @function
 * 
 */
Trans.prototype.closeProject = function() {
	if (typeof this.project == 'undefined') {
		return false
	}
	if (typeof this.grid.destroy() == 'function') this.grid.destroy();
	this.unInitFileNav();
	this.unInitLocalStorage();
	this.project = {};
	this.init();
	ui.tableCornerHideLoading(true);
	ui.closeAllChildWindow();
	ui.ribbonMenu.clear();
	ui.clearActiveCellInfo();
	ui.clearPathInfo();
	ui.setWindowTitle("");
	trans.clearFooter();
	this.initTable();
	this.gridIsModified(false);
}



/**
 * Generates new dictionary table
 * @function
 * @returns {object} references object (trans.project.references)
 */
Trans.prototype.generateNewDictionaryTable = function() {
	var thisID = "Common Reference";
	if (typeof this.project.files[thisID] !== 'undefined') return this.project.files[thisID];
	
	
	if (typeof this.project.references == 'object') {
		console.log("trans.project.reference is an object");
		for (var fileId in this.project.references) {
			this.project.files[fileId] = this.project.references[fileId];
		}
	} else {
		console.log("trans.project.reference is not an object");

		var templatePath 	= this.getTemplatePath()
		var templateObj 	= this.loadJSONSync(templatePath);
		console.log("template obj : ", templateObj);
		if (Boolean(templateObj) !== false) {
			this.project.references = templateObj.project.references;
			console.log("assigning reference : ", this.project.references);
			
		}

		this.mergeReference(trans);
		
		
	}
	return this.project.references;
	
}

/**
 * Initialize a new project
 * @function
 * @param  {object} options 
 * force
 * selectedFile {array}
 */
Trans.prototype.createProject = function(options) {
	console.log("running trans.createProject");
	if (this.isLoadingFileList) return false;
	if (this.gameFolder == "") return false;
	
	var trans 				= this;
	options 				= options||{};
	options.force 			= options.force||"";
	options.selectedFile 	= options.selectedFile||"";
	options.onAfterLoading 	= options.onAfterLoading||function(responseData, event) {}; // eslint-disable-line
	options.options 		= options.options||{};
	this.isLoadingFileList 	= true;
	
	ui.showLoading();
	
	var thisArgs = {
			gameFolder		:this.gameFolder,
			selectedFile	:options.selectedFile,
			gameEngine		:this.gameEngine,
			gameTitle		:this.gameTitle,
			projectId		:this.projectId,
			skipElement		:this.skipElement,
			indexOriginal	:0,
			indexTranslation:1,
			force			:options.force,
			rpgTransFormat	:trans.config.rpgTransFormat,
			options			:options.options
		}
	console.log("Sending this args to loadGameInfo.php : ", thisArgs);
	php.spawn("loadGameInfo.php", {
		args:thisArgs,
		onData: function(buffer) {
			ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput'});
		},
		onError:function(buffer) {
			ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput', classStr:'stderr'});
		},
		
		onDone : function(data) {
			console.log("onDone event defined from trans.js");
			if (common.isJSON(data)) { 
				trans.project = data;
				trans.currentFile = "";
				
				trans.gameTitle 	= data.gameTitle;
				trans.gameFolder 	= data.loc;
				trans.projectId 	= data.projectId;
				trans.gameEngine 	= data.gameEngine;
				trans.editorName	= "Translator++";
				trans.editorVersion = nw.App.manifest.version;
				trans.generateHeader();
				trans.sanitize();
				
				//trans.removeAllDuplicates();
				trans.dataPadding();
				sys.updateLastOpenedProject();
				trans.autoSave();
				//JSON.parse(data);
				console.log(data);
				ui.setStatusBar(1, trans.gameTitle);
				
				/**
				 * Executed when a project is created
				 * @event Trans#projectCreated
				 * @param  {Trans} trans - Instance of the current trans data
				 * @param  {Object} data
				 */
				trans.trigger("projectCreated", trans, data);
				if (typeof options.onAfterLoading == 'function') {
					options.onAfterLoading.call(trans, data, this);
				}
				trans.fileListLoaded = true;	
				trans.isLoadingFileList = false;
				ui.showCloseButton();
			} else {
				// try to find link from console response
				var loadedData = $("<div>"+data+"</div>");
				var initPath = loadedData.find("#initialDataPath").attr("data-path");
				console.log("found initPath : "+initPath);
				//var initData = loadedData.find("#initialData").text();
				//console.log(JSON.parse(initData));
				if (Boolean(initPath) == false) {
					ui.loadingProgress("Error!", "Can not successfully parse your game! Read the documentation here: https://dreamsavior.net/?p=1311", 
					{consoleOnly:true, mode:'consoleOutput'});
					ui.showCloseButton();
					trans.fileListLoaded = false;	
					trans.isLoadingFileList = false;
					return;					
				}

				try {
					fs.readFile(initPath, function (err, rawData) {
						if (err) {
							console.log("error opening file : "+initPath);
							ui.loadingProgress("Error!", "Error opening file : "+initPath, 
								{consoleOnly:true, mode:'consoleOutput'});
							ui.showCloseButton();
							trans.fileListLoaded = false;	
							trans.isLoadingFileList = false;							
							throw err;
						} else {
							var strData = rawData.toString();
							var data = {};
							try {
								data = JSON.parse(strData);
							} catch (err) {
								ui.loadingProgress(t("Error!"), t("Error processing init file : ")+initPath+"\n"+err, 
									{consoleOnly:true, mode:'consoleOutput'});
								ui.showCloseButton();
								trans.isLoadingFileList = false;
								return false;	
							}
							
							if (Boolean(data['files']) == false) {
								ui.loadingProgress(t("Error!"), t("Error! File list not found in init file at : ")+initPath, 
									{consoleOnly:true, mode:'consoleOutput'});
								ui.loadingProgress(t("Error!"), t("This means that your game was not successfully parsed. Please visit https://dreamsavior.net/?p=1311 for possible solution for this issue."), 
									{consoleOnly:true, mode:'consoleOutput'});
								ui.showCloseButton();
								trans.isLoadingFileList = false;
								return false;	
							}
							
							trans.project 		= data;
							trans.currentFile 	= "";
							
							trans.gameTitle 	= data.gameTitle;
							trans.gameFolder 	= data.loc;
							trans.projectId 	= data.projectId;
							trans.gameEngine 	= data.gameEngine;
							trans.editorName	= "Translator++";
							trans.editorVersion = nw.App.manifest.version;
							trans.generateHeader();
							trans.sanitize();
							
							//trans.removeAllDuplicates();
							
							trans.dataPadding();
							sys.updateLastOpenedProject();
							trans.autoSave();
							//JSON.parse(data);
							console.log(data);
							ui.setStatusBar(1, trans.gameTitle);


							trans.trigger("projectCreated", trans, data);
							if (typeof options.onAfterLoading == 'function') {
								options.onAfterLoading.call(trans, data, this);
							}
							trans.fileListLoaded = true;	
							trans.isLoadingFileList = false;
							ui.loadingProgress(t("Done!"), t("All done!"), {consoleOnly:false, mode:'consoleOutput'});
							ui.loadingEnd();
						}
					});		
				} catch (error)	{
					console.log("error opening file : "+initPath);
					ui.loadingProgress(t("Error!"), t("Error opening file : ")+initPath, {consoleOnly:false, mode:'consoleOutput'});
					ui.loadingProgress(t("Error!"), error, {consoleOnly:true, mode:'consoleOutput'});
					ui.loadingEnd();
					trans.fileListLoaded = false;	
					trans.isLoadingFileList = false;
					
				}

				var $tmpPath = loadedData.find("#tmpPath");
				if ($tmpPath.length > 0) {
	
					ui.showOpenCacheButton($tmpPath.text());
					
				}
				
				
			}
			
		}
	});
}

Trans.prototype.procedureCreateProject =function(gamePath, options) {
	console.log("running trans.procedureCreateProject");
	console.log(arguments);
	if (typeof gamePath == 'undefined') return false;
	options 				= options||{};
	options.selectedFile 	= options.selectedFile||"";
	options.force 			= options.force||"";
	options.projectInfo 	= options.projectInfo||{
								id		:"",
								title	:""
							}
	options.options 		= options.options||{};
	options.gameEngine 		= options.gameEngine||"";
	
	//console.log(options);
	this.closeProject();
	
	this.projectId 	= options.projectInfo.id;
	this.gameTitle 	= options.projectInfo.title;
	this.gameFolder = gamePath;
	this.gameEngine = options.gameEngine;
	
	//console.log("=======================");
	//console.log("Running trans.initFileNav() with current trans : ");
	//console.log(trans);
	
	// close newProject dialog box if any
	ui.newProjectDialog.close();
	this.createProject({
		selectedFile:options.selectedFile,
		force:options.force,
		onAfterLoading:async function(rawData) { // eslint-disable-line
			this.drawFileSelector();
			// record the project creation options
			this.project.options = this.project.options || {};
			this.project.options.init = options.options || {};
			if (engines.hasHandler('onAfterCreateProject')) {
				await common.wait(100);
				await engines.handler('onAfterCreateProject').call(this, gamePath, options);
			}			
		},
		options:options.options
	});	
	
	
}

Trans.prototype.getRow = function(fileData, keyword) {
	// locate a row number of a key
	// returns row number
	if (typeof keyword !== 'string') return undefined;
	console.log("Get row", arguments);
	if (fileData.indexIsBuilt == false) {
		console.log("building index for the first time");
		this.buildIndexFromData(fileData);
	}
	fileData.indexIds = fileData.indexIds || {};
	
	console.log("getRow result is : ", fileData.indexIds[keyword]);
	return fileData.indexIds[keyword];
}

/**
 * update current trans with new jsonData
 * @param {Trans} jsonData - jsonData must contains jsonData.project.files
 * @param {*} options 
 * @returns {Trans} - An updated trans data
 */
Trans.prototype.updateProject = function(jsonData, options) {
	/* update current trans with new jsonData
		jsonData must contains :
		jsonData.project.files
	*/
	if (typeof jsonData == 'string') jsonData = JSON.parse(jsonData);
	options = options||{};
	options.onAfterLoading 	= options.onAfterLoading||function(type, responseData, event) {}; 
	options.onSuccess 		= options.onSuccess||function(responseData, event) {};
	options.onFailed 		= options.onFailed||function(responseData, event) {};
	options.filePath 		= options.filePath||"";	
	options.purgeNonExistingData ||= false;
	
	jsonData.project = jsonData.project || {};
	jsonData.project.files = jsonData.project.files || {};
	
	const projectCopy 	= jsonData;
	const oldProject 	= this.getSaveData();
	const oldFiles 		= oldProject.project.files||{};
	console.log("Base data", jsonData);
	console.log("Preserved current files", oldFiles);
	projectCopy.project.files = jsonData.project.files;
	
	
	for (let file in jsonData.project.files) {
		var thisFile = projectCopy.project.files[file];
		if (!oldFiles[file]) continue;
		if (!oldFiles[file].data) continue;
		if (oldFiles[file].data.length < 1) continue;
		console.log("%cprocessing file", "color:aqua", file);
		
		if (options.purgeNonExistingData) {
			// new trans as pointer, good if we don't want to preserve existing record
			for (let rowId=0; rowId < thisFile.data.length; rowId++ ) {
				let key = thisFile.data[rowId][0];
				var oldFileRow = this.getRow(oldFiles[file], key);
				console.log("%cKey", "color:aqua", key);
				console.log("%coldFileRow", "color:aqua", oldFileRow);
				if (typeof oldFileRow == 'undefined') continue;
				if (!oldFiles[file].data[oldFileRow]) continue;
	
				for (let x=1; x<oldFiles[file].data[oldFileRow].length; x++) {
					//console.log("%c--Assigning ", "color:aqua",  oldFiles[file].data[oldFileRow][x], "to col", x);
					thisFile.data[rowId][x] = oldFiles[file].data[oldFileRow][x];
				}
			}
		} else {
			// preserve the old record that not exist in the newly updated data
			let thisOldFile = oldFiles[file];
			let contextCounter = 0;
			for (let rowId=0; rowId < thisOldFile.data.length; rowId++ ) {
				if (!thisOldFile.data[rowId]?.length) continue;
	
				let key = thisOldFile.data[rowId][0];
				if (!key) continue;
				var fileRow = this.getRow(thisFile, key);
				// console.log("%cKey", "color:aqua", key);
				// console.log("%coldFileRow", "color:aqua", fileRow);
				if (typeof fileRow == 'undefined') {
					//console.log("%cHandling non existing record", "color:aqua", thisOldFile.data[rowId]);
					let newLen = thisFile.data.push(thisOldFile.data[rowId]);
					let newIndex = newLen-1;
					// assign indexIds
					thisFile.data.indexIds ||= {};
					thisFile.data.indexIds[key] = newIndex;

					// assing context
					thisFile.context[newIndex] = ["RefreshCarryover/"+contextCounter]

					// assign cellInfo
					if (thisOldFile.cellInfo?.[rowId]) {
						thisFile.cellInfo ||= [];
						thisFile.cellInfo[newIndex] = thisOldFile.cellInfo[rowId]
					}
					if (thisOldFile.comments?.[rowId]) {
						thisFile.comments ||= [];
						thisFile.comments[newIndex] = thisOldFile.comments[rowId]
					}
					if (thisOldFile.parameters?.[rowId]) {
						thisFile.parameters ||= [];
						thisFile.parameters[newIndex] = thisOldFile.parameters[rowId]
					}
					if (thisOldFile.tags?.[rowId]) {
						thisFile.tags ||= [];
						thisFile.tags[newIndex] = thisOldFile.tags[rowId]
					}
					contextCounter++;
					continue;
				}
	
				// for (let x=1; x<thisOldFile.data[rowId].length; x++) {
				// 	//console.log("%c--Assigning ", "color:aqua",  thisOldFile.data[rowId][x], "to col", x);
				// 	thisFile.data[fileRow][x] = thisOldFile.data[rowId][x];
				// }
				// console.log("%c--Assigning ", "color:aqua",  thisOldFile.data[rowId], "to row", rowId);
				
				// assign data. Since the key is identical, we can safely copy the entire row
				thisFile.data[fileRow] = thisOldFile.data[rowId]
				// assign cellInfo
				if (thisOldFile.cellInfo?.[rowId]) {
					thisFile.cellInfo ||= [];
					thisFile.cellInfo[fileRow] = thisOldFile.cellInfo[rowId]
				}
				if (thisOldFile.comments?.[rowId]) {
					thisFile.comments ||= [];
					thisFile.comments[fileRow] = thisOldFile.comments[rowId]
				}
				if (thisOldFile.parameters?.[rowId]) {
					thisFile.parameters ||= [];
					thisFile.parameters[fileRow] = thisOldFile.parameters[rowId]
				}
				if (thisOldFile.tags?.[rowId]) {
					thisFile.tags ||= [];
					thisFile.tags[fileRow] = thisOldFile.tags[rowId]
				}
			}
		}

	}
	
	console.log("updatedProject", projectCopy);
	return projectCopy;
}

Trans.prototype.updateProject2 = function(updatedTrans, oldTrans = this.getSaveData()) {
	return updatedTrans
}


Trans.prototype.openFromTransObj = function(jsonData, options) {
	// open trans object from parsed jsonData or string
	if (typeof jsonData == 'string') jsonData = JSON.parse(jsonData);
	
	options = options||{};
	options.onAfterLoading 	= options.onAfterLoading||function(type, responseData, event) {};
	options.onSuccess 		= options.onSuccess||function(responseData, event) {};
	options.onFailed 		= options.onFailed||function(responseData, event) {};
	options.filePath 		= options.filePath||"";
	options.isNew 			= options.isNew || false;
	
	if (options.isNew) {
		jsonData = this.initTransData(jsonData);
		console.log(jsonData);
	}
	
	this.currentFile = options.filePath;
	this.applySaveData(jsonData);
	this.sanitize();
	this.grid.render();
	sys.insertOpenedFileHistory();
	// apply config to $DV.config;
	try {
		$DV.config.sl = this.project.options.sl||"ja";
		$DV.config.tl = this.project.options.tl||"en";
	} catch (e) {
		$DV.config.sl = "ja";
		$DV.config.tl = "en";
	}			
	if (typeof options.onSuccess == 'function') options.onSuccess.call(this, jsonData);
	if (typeof options.onAfterLoading == 'function') options.onAfterLoading.call(this, "success", jsonData);
	
	this.updateStagingInfo();
	
	this.isOpeningFile = false;
	ui.hideBusyOverlay();
	if (jsonData.project.selectedId) {
		this.selectFile(jsonData.project.selectedId);
	} else {
		this.selectFile($(".fileList .data-selector").eq(0));
	}

}



Trans.prototype.detectFormat = async function(path) {
	if (!path) return false;
	if (nwPath.extname(path).toLowerCase() == ".tpp") return "tpp";
	if (nwPath.extname(path).toLowerCase() == ".trans") return "trans";


	return false;
}

Trans.prototype.open = async function(filePath, options) {
	filePath = filePath||this.currentFile;
	if (filePath == "" || filePath == null || typeof filePath == 'undefined') return false;
	console.log("opening project : ", filePath);
	
	if (await this.detectFormat(filePath) == "tpp") {
		return this.importTpp(filePath);
	}
	


	var trans 				= this;
	options 				= options||{};
	options.onAfterLoading 	= options.onAfterLoading||function(type, responseData, event) {};
	options.onSuccess 		= options.onSuccess||function(responseData, event) {};
	options.onFailed 		= options.onFailed||function(responseData, event) {};
	
	trans.isOpeningFile = true;
	
	ui.showBusyOverlay();
	fs.readFile(filePath, function (err, data) {
		if (err) {
			//throw err;
			console.log(err);
			alert(t("error opening file (open): ")+filePath+"\r\n"+err);
			if (typeof data != 'undefined') {
				data = data.toString();
				if (typeof options.onFailed =='function') options.onFailed.call(trans, data);
				if (typeof options.onAfterLoading =='function') options.onAfterLoading.call(trans, "error", data);
			}
			ui.hideBusyOverlay();
		} else {
			data 				= data.toString();
			var jsonData 		= {};
			try {
				jsonData 		= JSON.parse(data);
			} catch (e) {
				console.warn("Failed to parse JSON data");
				alert("Failed to parse JSON data.\nThe .trans file is corrupted.");
				if (typeof options.onAfterLoading == 'function') options.onAfterLoading.call(trans, "Failed", jsonData);
				trans.isOpeningFile = false;
				ui.hideBusyOverlay();	
				return;			
			}
			
			console.log(jsonData);
			trans.currentFile 	= filePath;
			trans.applySaveData(jsonData);
			trans.sanitize();
			trans.grid.render();
			sys.insertOpenedFileHistory();
			// apply config to $DV.config;
			try {
				$DV.config.sl = trans.project.options.sl||"ja";
				$DV.config.tl = trans.project.options.tl||"en";
			} catch (e) {
				$DV.config.sl = "ja";
				$DV.config.tl = "en";
			}			
			if (typeof options.onSuccess == 'function') options.onSuccess.call(trans, jsonData);
			if (typeof options.onAfterLoading == 'function') options.onAfterLoading.call(trans, "success", jsonData);
			trans.isOpeningFile = false;
			ui.hideBusyOverlay();
			if (jsonData.project.selectedId) {
				trans.selectFile(jsonData.project.selectedId);
			} else {
				trans.selectFile($(".fileList .data-selector").eq(0));
			}

			trans.gridIsModified(false);

			// open infobox
			trans.project.options = trans.project.options || {}
			console.log("displaying info ")
			if (Boolean(trans.project.options.info) && trans.project.options.displayInfo) {
				ui.showPopup("infobox_"+trans.project.projectId, trans.project.options.info, {
					title		: "Project's Info",
					allExternal	: true,
					HTMLcleanup	: true
					});
			}
			
			// eval whether has error on paths
			if (trans.isCacheError()) {
				ui.addIconOverlay($(".button-properties"), "attention")
				$(".button-properties").attr("title", "Project properties - Staging path error!")
			} else {
				ui.clearIconOverlay($(".button-properties"))
				$(".button-properties").attr("title", "Project properties");
			}
		}
	});
}

Trans.prototype.isCacheError = function() {
	trans.project.cache = trans.project?.cache || {}
	if (trans.project.cache.cachePath) {
		if (common.isDir(trans.project.cache.cachePath) == false) {
			return true;
		}
	}
	
	return false;
}

Trans.prototype.getSl = function() {
	// get source language
	this.project.options 	= this.project.options || {}
	sys.config.default 		= sys.config.default || {};
	var sl 					= this.project.options.sl || sys.config.default.sl || $DV.config.sl;
	return sl;
}
Trans.prototype.getTl = function() {
	// get target language
	this.project.options 	= this.project.options || {}
	sys.config.default 		= sys.config.default || {};
	var tl 					= this.project.options.tl || sys.config.default.tl || $DV.config.tl;
	return tl;
}



//================================================================
//
// HANDLING SAVE & LOAD DATA
//
//================================================================
Trans.prototype.applyNewData = function(newData) {
	newData = newData||[[]];
	
	for (var row=0; row<newData.length; row++) {
		this.data[row] = newData[row];
	}
}

Trans.prototype.applyNewHeader = function(newHeader) {
	newHeader = newHeader||[];
	
	for (var header=0; header<newHeader.length; header++) {
		this.colHeaders[header] = newHeader[header];
	}
}

Trans.prototype.generateId = function() {
	return common.makeid(10);
}

Trans.prototype.initTransData = function(transData) {
	// initialize new trans data created by other application
	transData 		= transData||{};
	var template 	= JSON.parse(fs.readFileSync("data/template.trans"));
	// remove identity from template
	template.project ||= {}
	template.project.projectId 	= undefined;
	template.project.buildOn 	= undefined;
	
	var result 		= common.mergeDeep(template, transData);
	
	result.project 				= result.project || {}
	result.project.options		= result.project.options || {}
	result.project.options.init = result.project.options.init || {}
	result.project.projectId 	= result.project.projectId || this.generateId(10);
	result.project.buildOn 		= result.project.buildOn || common.formatDate(Date.now());
	result.project.editorVersion = nw.App.manifest.version;
	result.project.editorName 	= "Translator++";
	
	return result;
}

Trans.prototype.createFileData = function(fullPath, defaultData, options={}) {
	defaultData = defaultData || {}
	fullPath = fullPath.replace(/\\/g, "/");
	if (options?.leadSlash) {
		if (fullPath.charAt(0) !== "/") fullPath = "/"+fullPath;
	}
	
	defaultData.basename 		= defaultData.basename||nwPath.basename(fullPath),
	defaultData.filename 		= defaultData.filename||nwPath.basename(fullPath),
	defaultData.path 			= defaultData.path||fullPath,
	defaultData.relPath 		= defaultData.relPath||fullPath,
	defaultData.data			= defaultData.data||[[null]],
	defaultData.originalFormat 	= defaultData.originalFormat||"Autogenerated TRANS obj",
	defaultData.type			= defaultData.type||""
	defaultData.context			= defaultData.context||[]
	defaultData.tags			= defaultData.tags||[]
	
	if (typeof defaultData.extension == 'undefined') defaultData.extension = nwPath.extname(fullPath);
	if (typeof defaultData.dirname == 'undefined') 	defaultData.dirname = nwPath.dirname(fullPath);
	
	return defaultData;
}

Trans.prototype.validateTransData = function(transData) {
	// standarized transData
	
	/*
		adapt from two dimensional array
	*/
	var result = {};
	if (Array.isArray(transData)) {
		console.log("Case 1 - transData is array");
		let templateObj 	= this.loadJSONSync(this.getTemplatePath());
		let objName 		= "/main";
		templateObj.project.files[objName] = this.createFileData(objName, {data:transData});
		result = templateObj;
		return result;
	}

	/*
		object is in file structure
	*/	
	if (Boolean(transData.project)==false && Boolean(transData.files)==false && Array.isArray(transData.data)) {
		console.log("Case 1 - transData is file structured");
		let templateObj 	= this.loadJSONSync(this.getTemplatePath());
		let objName 		= transData.path||"/main";
		templateObj.project.files[objName] = this.createFileData(objName, {data:transData.data});
		result = templateObj;
		return result;
	}

	if ( Boolean(transData.project)==false && Boolean(transData.files)==true) {
		result.project = transData;
	} else {
		result = transData;
	}

	result.project.gameTitle 		= result.project.gameTitle||t("untitled project");
	result.project.gameEngine 		= result.project.gameEngine||"";
	result.project.projectId 		= result.project.projectId||"";
	result.project.buildOn			= result.project.buildOn || common.formatDate();
	result.project.files			= result.project.files || {};
	
	for (var id in result.project.files) {
		result.project.files[id] = this.createFileData(id, result.project.files[id])
	}
	
	return result;
}

/**
 * Normalize loaded column header
 * Apply default value & unchangeable value into trans.columns
 * Should be called each time trans files are loaded
 */
Trans.prototype.normalizeHeader = function() {
	for (var i=0; i<this.columns.length; i++) {
		if (i == this.keyColumn) {
			this.columns[i].validator 		= this.validateKey;
		}

		//this.columns[i].trimWhitespace 		= false;	
		this.columns[i].wordWrap			= true;	
	}
}

/**
 * Load the trans structured data into the current project
 * @param {Object} saveData - Trans structured data
 * @returns {Object} - Instance of Trans
 */
Trans.prototype.applySaveData = function(saveData) {
	console.log("entering trans.applySaveData");
	console.log(saveData);
	saveData 			= saveData||{};
	saveData 			= this.validateTransData(saveData);
	this.data 			= saveData.data||[[null]];
	//trans.data 		= [[]];
	this.columns 		= saveData.columns||this.default.columns||[];
	this.normalizeHeader();
	this.colHeaders 	= saveData.colHeaders||this.default.colHeaders||[];
	this.project 		= saveData.project||{};
	//this.indexIds 		= saveData.indexIds||{};
	
	// FILLING ROOT VARIABLE based on game project
	this.gameTitle 		= saveData.project.gameTitle||"";
	this.gameEngine 	= saveData.project.gameEngine||"";
	this.projectId 		= saveData.project.projectId||"";
	this.gameFolder 	= saveData.project.loc||"";

	// detecting fileList
	if (saveData.fileListLoaded == false) {
		try {
			if (typeof saveData.project.files !== "undefined") saveData.fileListLoaded = true;
		}
		catch(err) {
			saveData.fileListLoaded = false;
		}
	}	
	this.resetIndex();
	this.fileListLoaded = saveData.fileListLoaded||false;	

	this.initFileNav();
	this.refreshGrid();
	ui.setWindowTitle();
	ui.setStatusBar(1, this.gameTitle);	
	//engines.handler('onLoadTrans').apply(this, arguments);
	return this;
}

/**
 * Create a clone structure of the trans object that ready to be saved
 * @param {Object} options
 * @param {String} options.type - Type of the returned data (""||"json"||"lz") 
 * @returns {(Object|String)} - A clone of trans object
 */
Trans.prototype.getSaveData = function(options) {
	options 		= options||{};
	options.filter 	= options.filter || [];
	options.type	= options.type || ""; // ""||"json"||"lz"
	
	if (empty(this.project)) return;
	if (empty(this.project.files)) return;
	this.project.projectId = this.project.projectId || common.makeid(10);
	//if (!this.project.projectId) return;
	var projectClone = JSON.parse(JSON.stringify(this.project))||{};
	
	if (options.filter.length > 0) {
		console.log("filtering saved object", options);
		if (typeof this.project !== 'undefined') {
			if (typeof this.project.files !== 'undefined') {
				projectClone.files = {};
				for (var i=0; i<options.filter.length; i++) {
					var thisId = options.filter[i];
					console.log("testing "+thisId, this.project.files[thisId], this.project.files);
					if (typeof this.project.files[thisId] == 'undefined') continue;
					console.log("exist, assigning "+thisId);
					projectClone.files[thisId] = JSON.parse(JSON.stringify(this.project.files[thisId]));
				}
			}
		}
	}
	
	var saveData = {};
	//saveData.data 		= this.data||[];
	saveData.data 			= [[null]];
	saveData.columns 		= this.columns||[];
	saveData.colHeaders 	= this.colHeaders||[];
	saveData.project 		= projectClone;
	//saveData.indexIds 		= this.indexIds;
	saveData.fileListLoaded = this.fileListLoaded;
	
	
	// get column width
	for (let i=0; i<this.grid.getColHeader().length; i++) {
		if (!saveData.columns[i]) continue;
		saveData.columns[i].width = this.grid.getColWidth(i);
	}	
	
	// strip out reference data
	for (var fileId in saveData.project.files) {
		if (saveData.project.files[fileId].type == 'reference') {
			saveData.project.references 		= saveData.project.references||{};
			saveData.project.references[fileId] = JSON.parse(JSON.stringify(saveData.project.files[fileId]));
			delete saveData.project.files[fileId];
		}
	}
	
	if (options.type == "json") return JSON.stringify(saveData);
	return saveData;
}

/**
 * Compress an object
 * @async
 * @param {(Buffer|String|Object)} data
 * @param {*} options
 * @return {Promise<string>} - compressed data
 */
Trans.prototype.compress = async function(data, options) {
	options = options || {};
	var buff = Buffer.from([]);
	if (Buffer.isBuffer(data)) {
		buff = data;
	} else if (typeof data == "string") {
		buff = Buffer.from(data);
	} else if (typeof data == "object" && !empty(data)) {
		buff = Buffer.from(JSON.stringify(data));
	} else {
		// generate buffer from trans
		buff = Buffer.from(JSON.stringify(this.getSaveData()));
	}

	return common.gzip(buff, options);
	
}

/**
 * Uncompress a string
 * @async
 * @param {(Buffer|String)} data - Compressed string
 * @param {*} options 
 * @returns {Promise<string>} - uncompressed string
 */
Trans.prototype.uncompress = async function(data, options) {
	options = options || {};
	var buff = Buffer.from([]);
	if (Buffer.isBuffer(data)) {
		buff = data;
	} else if (typeof data == "string") {
		buff = Buffer.from(data);
	} else {
		return console.error("Can not uncompress from data:", data, "Only buffer or string is accepted");
	}

	return common.gunzip(buff, options);
}

/**
 * @async
 * @param  {(Buffer|String)} source - the source object
 */
Trans.prototype.from = async function(source) {
	var str = "";
	var obj;

	if (Buffer.isBuffer(source)) {
		if (source.slice(0, 10).join(",") == "31,139,8,0") {
			// gziped
			str = await common.gunzip(source);
			str = str.toString();
		} else {
			str = source.toString();
		}
	} else if (typeof source == "string") {
		str = source;
	}
	
	if (str) {
		if (JSON.isJSON(str) == false) return console.error("unknown format :", source);
		obj = JSON.parse(str);
	}

	return obj;
}

/**
 * Generate backup from given path of trans file
 * @param {String} saveFile - Path to the file
 */
Trans.prototype.createBackup = async function(saveFile) {
	var backupLevel = parseInt(sys.getConfig("backupLevel"));
	if (!sys.getConfig("autoBackup")) return;
	if (!backupLevel) return;
	if (!await common.isFileAsync(saveFile)) return console.warn("no such file ", saveFile);
	if ([".trans", ".tpp"].includes(nwPath.extname(saveFile).toLowerCase()) == false) return;

	// remove last file
	await common.unlink(saveFile+`.${backupLevel}.bak`);

	for (var i=backupLevel-1; i>=0; i--) {
		if (i==0) {
			await common.rename(saveFile, saveFile+`.${i+1}.bak`);
		} else {
			if (await common.isFileAsync(saveFile+`.${i}.bak`) == false) continue;
			await common.rename(saveFile+`.${i}.bak`, saveFile+`.${i+1}.bak`)
		}
	}
	
}

/**
 * Save project into .trans file with the new filename
 * This function will open a blocking save dialog.
 * @since 4.3.16
 * @async
 * @param  {String} targetFile - Path to the file
 * @param  {Object} options - Object of the options
 * @param  {String} options.initiator - Who called the function user||auto
 * @param  {Function} options.onAfterLoading - Callback after the process is done
 * @param  {Function} options.onSuccess - Callback after the file is saved successfully
 * @param  {Function} options.onFailed - Callback when the error is occured
 * @returns {Promise<string>} - The path where the file was saved
 */
Trans.prototype.saveAs = async function(targetFile, options) {
	var target = await ui.saveAs(targetFile || trans.currentFile || "");
	console.log("Saving into : ", target);
	if (!target) return "";

	return await this.save(target, options);
}

/**
 * Save project into .trans file
 * @async
 * @param  {String} targetFile - Path to the file
 * @param  {Object} options - Object of the options
 * @param  {String} options.initiator - Who called the function user||auto
 * @param  {Function} options.onAfterLoading - Callback after the process is done
 * @param  {Function} options.onSuccess - Callback after the file is saved successfully
 * @param  {Function} options.onFailed - Callback when the error is occured
 * @returns {Promise<string>} - The path where the file was saved
 */
Trans.prototype.save = async function(targetFile, options) {
	targetFile = targetFile||this.currentFile;
	if (!targetFile) return await this.saveAs(targetFile, options);
	
	var trans 				= this;
	options 				= options||{};
	options.initiator 		= options.initiator||"user";
	options.filter 			= options.filter || [];
	options.onAfterLoading 	= options.onAfterLoading||function(responseData, event) {};
	options.onSuccess 		= options.onSuccess||function(responseData, event) {};
	options.onFailed 		= options.onFailed||function(responseData, event) {};
	
	if (trans.isSavingFile) return console.warn("Trans.prototype.save() is busy! Please wait until the previous save procedure to be completed before triggering another one!");

	// data to save
	console.log("Saving data to : "+targetFile);
	var saveData = trans.getSaveData(options);
	console.log(saveData);
	
	
	trans.isSavingFile = true;
	ui.saveIndicatorStart();
	if (options.initiator == "user") await this.createBackup(targetFile);
	return new Promise((resolve, reject)=> {
		fs.writeFile(targetFile, JSON.stringify(saveData), (err) => {
			if (err) {
				if (typeof options.onFailed =='function') options.onFailed.call(trans, saveData, targetFile);
				ui.saveIndicatorEnd()
				console.warn("Failed to save to : ", targetFile, err );
				reject();
			} else {
				if (options.initiator !== "auto") {
					console.log(targetFile+' successfully saved!');
					sys.insertOpenedFileHistory(targetFile,saveData.project.projectId,saveData.project.gameTitle, options.initiator);
					this.currentFile = targetFile;
					trans.gridIsModified(false);
					ui.setWindowTitle();
				} 
				
				if (typeof options.onSuccess == 'function') options.onSuccess.call(trans, saveData, targetFile);
				resolve(targetFile);
			}

			options.onAfterLoading.call(trans, saveData, targetFile);
			
			setTimeout(function(){ 
				ui.saveIndicatorEnd()
			}, 1000);
			
			trans.isSavingFile = false;
		});	
	})
	
	
}

/**
 * Generating chache path
 */
Trans.prototype.generateCachePath = function() {
	this.project.cache = this.project.cache || {}
	this.project.cache.cacheID 		= this.project.cache.cacheID||this.project.projectId;
	if (!this.project.cache.cacheID) this.project.cache.cacheID = this.project.projectId = common.makeid(10)
		console.log("Joining", sys.config.stagingPath, this.project.cache.cacheID);
	this.project.cache.cachePath 	= this.project.cache.cachePath || nwPath.join(sys.config.stagingPath, this.project.cache.cacheID);
	
	try {
		fs.mkdirSync(this.project.cache.cachePath, {recursive:true})
	} catch (e) {
		console.warn(e);
	}

}

/**
 * Trigger auto save procedure
 * @async
 * @param {Object} options 
 * @param {String} [options.initiator=auto] - Who call this function
 * @param {Function} options.onSuccess - Called when success
 * @returns {String} - Path to the saved file if success
 */
Trans.prototype.autoSave = async function(options) {
	options = options||{};
	
	if (typeof this.project == 'undefined') {
		options.onSuccess = options.onSuccess || function(){};
		options.onSuccess.call(this);
		return false;
	}
	try {
		if (!this.project.cache.cachePath) {
			this.generateCachePath();
		} else {
			if (!common.isDir(this.project.cache.cachePath)) this.generateCachePath();
		}
	} catch (e) {
		console.warn(e)
	}
	
	options.initiator = "auto";
	var path = nwPath.join(this.project.cache.cachePath,"autosave.json");
	await this.save(path, options);	
	return path;
}

/**
 * Return processed fileData on success. A transmutable function.
 * @param  {Object} fileData - Object of the file (trans.project.files[filePath])
 * @param  {Boolean} force - Force rebuilding the index
 * @returns {Object} fileData (mutable)
 */
Trans.prototype.buildIndexFromData = function(fileData, force) {
	// fileData is file object :
	// ex. trans.project.files['main']
	// return processed fileData on success
	// transmutable function
	var key = 0;
	if (typeof fileData !== 'object') return console.warn("fileData is not an object");
	if (Array.isArray(fileData.data) == false) return console.warn("fileData.data is not a valid array");
	if (fileData.indexIsBuilt && !force) return fileData;
	fileData.data = fileData.data || [];
	
	fileData.indexIds = fileData.indexIds || {}

	for (var row = 0; row<fileData.data.length; row++) {
		var thisRow = fileData.data[row];
		if (!thisRow[key]) continue;
		fileData.indexIds[thisRow[key]] = row;
	}
	fileData.indexIsBuilt= true
	return fileData;
	
}

/**
 * merge externalTrans into targetTrans
 * by default targetTrans = current project
 * @param {Object} externalTrans - External instance of Trans object to be exported
 * @param {Object} [targetTrans=this] - existing instance of Trans object
 * @param {Object} options 
 * @param {Boolean} options.overwrite - Will overwrite if the same file object exist
 * @param {Boolean} options.all - Will merge all file object if True
 * @param {Object} options.targetPair - Target files
 * @param {String[]} options.files - List of source files
 * @returns {Object} - merged trans
 */
Trans.prototype.mergeTrans = function(externalTrans, targetTrans, options) {
	// merge externalTrans into targetTrans
	// bydefault targetTrans = current project;
	var key = 0;
	
	targetTrans = targetTrans || this;
	targetTrans.project = targetTrans.project || {};
	targetTrans.project.files = targetTrans.project.files || {};
	
	externalTrans = externalTrans || {};
	externalTrans.project = externalTrans.project || {}
	externalTrans.project.files = externalTrans.project.files || {};
	
	options = options || {};
	options.overwrite 	= options.overwrite || false;
	options.targetPair 	= options.targetPair||{};
	options.files 		= options.files || [];
	options.all 		= options.all || false; // fetch all?
	
	if (options.all) options.targetPair =  externalTrans.project.files; // if all, doesn't use targetPair
	targetTrans = this.sanitize(targetTrans);
	
	var targetIsSelf = false;
	if (targetTrans instanceof Trans) targetIsSelf = true;
	
	
	console.log("running mergeTrans with args : ", arguments);
	//return;
	for (var id in options.targetPair) {
		var sourceFile = externalTrans.project.files[id];
		var targetFile = targetTrans.project.files[id];
		if (!targetFile) {
			// copy entire sourcefile into targetFile
			targetTrans.project.files[id] = common.clone(sourceFile);
			if (targetIsSelf) this.addFileItem(id, targetTrans.project.files[id]);
			continue;
		}
		console.log("merging data", id);
		// the real deal, merge the data
		if (Array.isArray(sourceFile.data)==false) continue;
		if (sourceFile.data.length == 0) continue;
		this.buildIndexFromData(targetFile);

		targetFile.context = targetFile.context||[];
		sourceFile.context = sourceFile.context||[];
		
		for (var row=0; row<sourceFile.data.length; row++) {
			var thisSourceRow = sourceFile.data[row];
			if (Boolean(thisSourceRow[key])==false) continue;
			
			var index = targetFile.indexIds[thisSourceRow[key]];
			if (typeof index == 'undefined') {
				index = targetFile.data.length;
			}
			targetFile.data[index] 		= common.clone(thisSourceRow);
			targetFile.context[index] 	= common.clone(sourceFile.context[row]);
			
		}
	}
	
	if (targetIsSelf)  {
		this.generateHeader(targetTrans);
		this.evalTranslationProgress();
		ui.fileList.reIndex();
		ui.initFileSelectorDragSelect();
	}
	
	this.refreshGrid();
	
	return targetTrans;
}


/**
 * Load JSON file in synchronous fashion
 * @param {String} filePath - Path to the JSON file
 * @param {Object} options 
 * @returns {Object} Loaded JSON object
 */
Trans.prototype.loadJSONSync = function(filePath, options) {
	if (filePath == "" || filePath == null || typeof filePath == 'undefined') return false;
	
	options = options||{};
	options.onAfterLoading = options.onAfterLoading||function(responseData, event) {};
	options.onSuccess = options.onSuccess||function(responseData, event) {};
	options.onFailed = options.onFailed||function(responseData, event) {};
	
	var fs = require('fs');
    var content = fs.readFileSync(filePath);	
	var resultStr = content.toString();
	var result = false;
	try {
		result = JSON.parse(resultStr);
		return result;
	} catch(e) {
		return result;
	}
}

/**
 * open JSON & Parse it. General purpose function
 * @async
 * @param {String} filePath - Path to the JSON file
 * @param {Object} options 
 * @param {Function} options.onSuccess - Called when success 
 * @param {Function} options.onFailed - Called when failed 
 */
Trans.prototype.loadJSON = async function(filePath, options) {
	// open JSON & Parse it
	// for general purposes
	var trans = this;

	if (filePath == "" || filePath == null || typeof filePath == 'undefined') return false;
	
	options = options||{};
	options.onAfterLoading 	= options.onAfterLoading||function(responseData, event) {};
	options.onSuccess 		= options.onSuccess||function(responseData, event) {};
	options.onFailed 		= options.onFailed||function(responseData, event) {};
	
	this.isOpeningFile = true;
	
	ui.showBusyOverlay();
	var jsonData;
	try {
		let loadedFile = await common.fileGetContents(filePath);
		jsonData = JSON.parse(loadedFile);
	} catch (e) {
		alert("Error loading file: "+filePath)
	}
	ui.hideBusyOverlay();
	trans.isOpeningFile = false;
	return jsonData;

	// fs.readFile(filePath, function (err, data) {
	// 	if (err) {
	// 		console.log("error opening file (loadJSON): "+filePath);
	// 		data = data.toString();
	// 		if (typeof options.onFailed =='function') options.onFailed.call(trans, data);
	// 		ui.hideBusyOverlay();

	// 		throw err;

	// 	} else {
	// 		data 			= data.toString();
	// 		try {
	// 			var jsonData 	= JSON.parse(data);
	// 			console.log(jsonData);
	// 		} catch (e) {
	// 			alert(t("Can not parse JSON data. Probably the file is corrupted."));
	// 			options.onFailed.call(trans, jsonData);
	// 			trans.isOpeningFile = false;
	// 			ui.hideBusyOverlay();
	// 			return;
	// 		}

			
	// 		if (typeof options.onSuccess == 'function') options.onSuccess.call(trans, jsonData);
	// 		trans.isOpeningFile = false;
	// 		ui.hideBusyOverlay();
			
	// 	}
	// });
}

/**
 * Import from file and replace or create object
 * @param {String|Trans} file - Trans file to be imported
 * @param {Object} options 
 * @param {Boolean} options.overwrite - Will overwrite if the same file object exist
 * @param {Boolean} options.all - Will merge all file object if True
 * @param {Object} options.targetPair - Target files
 * @param {String[]} options.files - List of source files
 * @param {Boolean} options.mergeData 
 */
Trans.prototype.importFromFile = async function(file, options) {
	// import from file and replace or create object
	// options.targetPair = {
	//		sourceKey : targetKey
	// }
	// or 
	// options.targetPair = {
	//		sourceKey : true  // same with sourceKey
	// }
	// 
	// if targetKey is not exist, then create one.

	options 			= options||{};
	options.overwrite 	= options.overwrite || false;
	options.targetPair 	= options.targetPair||{};
	options.files 		= options.files || [];
	options.mergeData	= options.mergeData || false;
	//if (Array.isArray(file) == false) file = [file];
	var trans = this;
	
	var data;

	if (!this.isTrans(file)) {
		data = await trans.loadJSON(file);
		data = trans.validateTransData(data);
	} else {
		data = file;
	}
			
	if (options.mergeData) {
		trans.mergeTrans(data, trans, options);
		return;
	}
	
	
	for (let sourceKey in options.targetPair) {
		if (typeof options.targetPair[sourceKey] !== 'string') {
			options.targetPair[sourceKey] = sourceKey;
		}
		
		try {
			//if (typeof trans.project.files[options.targetPair[sourceKey]] == 'undefined') continue;
			if (data.project.references[sourceKey] !== 'undefined') {
				trans.project.files[options.targetPair[sourceKey]] = data.project.references[sourceKey];
			}
			
			if (typeof data.project.files[sourceKey] == 'undefined') continue;
			trans.project.files[options.targetPair[sourceKey]] = data.project.files[sourceKey];
		
			console.log(t("create file list"), options.targetPair[sourceKey], trans.project.files[options.targetPair[sourceKey]]);
			trans.addFileItem(options.targetPair[sourceKey], trans.project.files[options.targetPair[sourceKey]]);
			
		} catch (e) {
			console.log(e);
			continue;
		}
		
	}
	ui.initFileSelectorDragSelect();
	trans.evalTranslationProgress();
	ui.fileList.reIndex();
	trans.refreshGrid();

	// trans.loadJSON(file, {
	// 	onSuccess : function(data) {
	// 		data = trans.validateTransData(data)
			
	// 		if (options.mergeData) {
	// 			trans.mergeTrans(data, trans, options);
	// 			return;
	// 		}
			
			
	// 		for (let sourceKey in options.targetPair) {
	// 			if (typeof options.targetPair[sourceKey] !== 'string') {
	// 				options.targetPair[sourceKey] = sourceKey;
	// 			}
				
	// 			try {
	// 				//if (typeof trans.project.files[options.targetPair[sourceKey]] == 'undefined') continue;
	// 				if (data.project.references[sourceKey] !== 'undefined') {
	// 					trans.project.files[options.targetPair[sourceKey]] = data.project.references[sourceKey];
	// 				}
					
	// 				if (typeof data.project.files[sourceKey] == 'undefined') continue;
	// 				trans.project.files[options.targetPair[sourceKey]] = data.project.files[sourceKey];
				
	// 				console.log(t("create file list"), options.targetPair[sourceKey], trans.project.files[options.targetPair[sourceKey]]);
	// 				trans.addFileItem(options.targetPair[sourceKey], trans.project.files[options.targetPair[sourceKey]]);
					
	// 			} catch (e) {
	// 				console.log(e);
	// 				continue;
	// 			}
				
	// 		}
	// 		ui.initFileSelectorDragSelect();
	// 		trans.evalTranslationProgress();
	// 		ui.fileList.reIndex();
	// 		trans.refreshGrid();
			
	// 	}
	// });
}


/**
 * Select a cell from grid
 * @param {Number} row 
 * @param {Number} column 
 * @returns {Boolean} True if success
 */
Trans.prototype.selectCell = function(row, column) {
	return this.grid.selectCell(row, column);
}





/**
 * Generate checksum for the original text
 * The checksume is 32bit representation of the original texts
 * So the app can determine whether the game is same or not by their respective original texts
 * @returns {String} 8 Byte of the project's checksum
 */
 Trans.prototype.getProjectChecksum = function() {
	if (this.project.checksum) return this.project.checksum;

	var sums = [];
	var allFiles = this.getAllFiles();
	allFiles.sort();
	for (var f in allFiles) {
		var thisFile = this.project.files[allFiles[f]];
		if (empty(thisFile)) continue;
		if (empty(thisFile.data)) continue;
		// exclude "*" path such as Common Reference
		if (thisFile.dirname == "*") continue;
		var textPool = [];
		for (var r=0; r<thisFile.data.length; r++) {
			if (!thisFile.data[r][this.keyColumn]) continue;
			textPool.push(thisFile.data[r][this.keyColumn]);
		}
		if (textPool.length < 1) continue;
		textPool.sort();
		sums.push(common.crc32String(JSON.stringify(textPool)));
	}
	this.project.checksum = common.crc32String(JSON.stringify(sums));
	return this.project.checksum;
}


/**
 * Export project into a TPP file
 * @function
 * @param {String} file - Path to the tpp file
 * @param {*} options 
 * @param {Function} options.onDone - Triggered when done
 */
 Trans.prototype.exportTPP = function(file, options) {
	// export translation to TPP
	if (typeof file=="undefined") return false;
	
	options = options||{};
	options.showDetail 	= options.showDetail || false;
	options.onDone 		= options.onDone||function() {};
	
	var autofillFiles = [];
	var checkbox = $(".fileList .data-selector .fileCheckbox:checked");
	for (var i=0; i<checkbox.length; i++) {
		autofillFiles.push(checkbox.eq(i).attr("value"));
	}
	options.files = options.files||autofillFiles||[];

	ui.saveIndicatorStart();

	trans.autoSave({
		onSuccess:function() {
			//ui.showLoading();
			php.spawn("saveTpp.php", {
				args:{
					path:file,
					password:options.password,
					gameFolder:trans.gameFolder,
					gameTitle:trans.gameTitle,
					projectId:trans.projectId,
					gameEngine:trans.gameEngine,
					files:options.files,
					exportMode:options.mode,
					rpgTransFormat:trans.config.rpgTransFormat
				},
				onData:function(buffer) {
					if (options.showDetail) ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput'});
					
				},
				onError:function(buffer) {
					if (options.showDetail) ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput', classStr:'stderr'});
					
				},
				onDone: function(data) {
					//console.log(data); 
					//ui.hideLoading(true);
					if (options.showDetail) {
						ui.loadingProgress(t("Finished"), t("All process finished!"), {consoleOnly:false, mode:'consoleOutput'});
					
						ui.showCloseButton();
						ui.LoadingAddButton(t("Open Explorer"), function() {
							common.openExplorer(file);
						});
					}
					ui.saveIndicatorEnd();
					options.onDone.call(trans, data);
				}
			})
			
		}
	});
	
}

/**
 * Load TPP file
 * @param {Trans} file - Path to the .tpp file
 * @param {Object} options 
 * @param {Function} options.onDone - Triggered when done 
 */
 Trans.prototype.importTpp = function(file, options) {
	if (typeof file=="undefined") return false;
	
	options = options||{};
	options.onDone = options.onDone||function() {};
	
	var doLoadTppToStage = function() {
		ui.showLoading();
		php.spawn("loadTpp.php", {
			args:{
				path:file,
				password:options.password
			},
			onData:function(buffer) {
				ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput'});
				
			},
			onError:function(buffer) {
				ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput', classStr:'stderr'});
				
			},
			onDone: function(data) {
				//console.log(data); 
				console.log("done")
				//ui.hideLoading(true);
				var saveFile = $(".console").find("output.cachepath").text();
				saveFile = nwPath.join(saveFile,"autosave.json");
				console.log("new cache path : ", saveFile);
				ui.loadingProgress(t("Loading"), t("Opening file!"), {consoleOnly:false, mode:'consoleOutput'});
				
				trans.open(saveFile, {
					onSuccess: function() {
						// writing new cache path
						trans.project.cache = trans.project.cache || {}
						trans.project.cache.cachePath = nwPath.dirname(saveFile);
						
						ui.loadingProgress(t("Loading"), t("Assigning new staging path : "+trans.project.cache.cachePath), {consoleOnly:false, mode:'consoleOutput'});
						
						ui.loadingProgress(t("Loading"), t("Success!"), {consoleOnly:false, mode:'consoleOutput'});
						ui.loadingProgress(t("Finished"), t("All process finished!"), {consoleOnly:false, mode:'consoleOutput'});
						ui.showCloseButton();
						options.onDone.call(trans, data);
						
					
					},
					onFailed: function() {
						ui.loadingProgress(t("Loading"), t("Failed!"), {consoleOnly:false, mode:'consoleOutput'});
						ui.loadingProgress(t("Finished"), t("All process finished!"), {consoleOnly:false, mode:'consoleOutput'});
						ui.showCloseButton();
						options.onDone.call(trans, data);					
					}
				});
				

			}
		});		
	}
	trans.closeProject();
	trans.autoSave({
		onSuccess:function() {
			doLoadTppToStage();	
		}
	});		
}

/**
 * Export current project
 * @param {String} file - Path to the file/folder
 * @param {Object} options 
 * @param {Function} options.onDone - Triggered when done
 * @param {String} options.mode - Export mode (ex. dir)
 * @param {String} options.dataPath - location of data path (data folder). Default is cache path
 * @param {String} options.transPath - location of the trans file.  Default is using autosave on cache folder
 * @param {String[]} options.files - List of the file(s) to be exported
 * @param {Object} options.options 
 * @param {String[]} options.options.filterTag - Filter of the tag 
 * @param {String} options.options.filterTagMode - Mode of the filter (whitelist||blacklist)
 */
 Trans.prototype.export = async function(file, options) {
	// export translation
	if (typeof file=="undefined") return false;
	
	options = options||{};
	options.options = options.options||{};
	

	console.log("Exporting with arguments:", arguments);

	//return console.log("Exporting project", arguments);
	options.mode 					= options.mode||"dir";
	options.onDone					= options.onDone||function() {};
	options.dataPath 				= options.dataPath || ""; // location of data path (data folder). Default is using cache
	options.transPath 				= options.transPath || ""; // location of .trans path to process. Default is using autosave on cache folder
	options.options.filterTag 		= options.options.filterTag|| options.filterTag ||[];
	options.options.filterTagMode 	= options.options.filterTagMode||options.filterTagMode||""; // whitelist or blacklist
	options.custom 					= options.custom || options.options.custom;
	delete options.options.custom; // delete options.options.custom to avoid confusion
	console.log("exporting project", arguments);
	
	var autofillFiles = [];
	var checkbox = $(".fileList .data-selector .fileCheckbox:checked");
	for (var i=0; i<checkbox.length; i++) {
		autofillFiles.push(checkbox.eq(i).attr("value"));
	}
	options.files = options.files||autofillFiles||[];

	// custom export handler
	let thisEngine = trans.project.gameEngine;
	if (typeof engines[thisEngine] !== 'undefined') {
		if (typeof engines[thisEngine].exportHandler == 'function') {
			var halt = await engines[thisEngine].exportHandler.apply(this, arguments);
			console.log("Is process halt?", halt);
			
			if (halt) {
				await this.projectHook.run("afterExport", options);
				return;
			}
		}	
	}

	var autosavePath = await trans.autoSave();
	ui.showLoading();
	trans.project.options = trans.project.options || {}
	trans.project.options.init = trans.project.options.init || {};
	var projectOption = Object.assign(trans.project.options.init, options.options);
	php.spawn("export.php", {
		args:{
			path		:file,
			gameFolder	:trans.gameFolder,
			gameTitle	:trans.gameTitle,
			projectId	:trans.projectId,
			gameEngine	:trans.gameEngine,
			files		:options.files,
			exportMode	:options.mode,
			options		:projectOption,
			rpgTransFormat:trans.config.rpgTransFormat,
			dataPath	:options.dataPath,
			transPath	:nwPath.resolve(options.transPath || autosavePath)
		},
		onData:function(buffer) {
			ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput'});
			
		},
		onError:function(buffer) {
			ui.loadingProgress(t("Loading"), buffer, {consoleOnly:true, mode:'consoleOutput', classStr:'stderr'});
			
		},
		onDone: async (data) => {
			//console.log(data); 
			console.log("done")
			//ui.hideLoading(true);
			ui.loadingProgress(t("Finished"), t("All process finished!"), {consoleOnly:false, mode:'consoleOutput'});
			
			//ui.showCloseButton();
			await this.projectHook.run("afterExport", options);
			ui.loadingEnd();
			options.onDone.call(trans, data);
		}
	})
	
}

/**
 * Revert the original data into a folder
 * Will replace the existing file on the destination directory
 * @param {String} destinationPath 
 * @param {Object} options 
 */
Trans.prototype.revertToOriginal = async function(destinationPath, options={}) {
	var bCopy = require('better-copy')
	if (!destinationPath) {
		destinationPath = await ui.openRevertToOriginalDialog();
	}

	options.dataPath = options.dataPath || "data"

	if (!destinationPath) return; // canceled

	ui.showLoading();
	ui.loadingProgress(0, "Reverting original data");

	var objects = this.project.files;
	if (this.getCheckedFiles().length > 0) objects = this.getCheckedObjects();

	var copied = 0;
	var processed = 0;
	var totalLength = Object.keys(objects).length || 1;
	ui.log(`Processing ${totalLength} file(s)`);
	for (var i in objects) {
		processed++;

		var stagingBaseFolder = this.getStagingDataPath();
		var stagingFile = this.getStagingFile(objects[i]);
		var relativePath = common.getRelativePath(stagingFile, stagingBaseFolder)
		await ui.loadingProgress(Math.round((processed/totalLength)*100), `Reverting : ${relativePath}`);

		if (! await common.isFileAsync(stagingFile)) {
			await ui.log(`File not found: ${stagingFile}`);
			continue;
		}

		await ui.log(`${processed}/${totalLength} Copying : ${stagingFile}`);
		await bCopy(stagingFile, nwPath.join(destinationPath, relativePath), {
			overwrite:true
		});
		copied++;
	}
	await ui.log(`Completed! ${copied} file(s) reverted to original!`);
	ui.LoadingAddButton("Open folder", function() {
			nw.Shell.showItemInFolder(nwPath.join(destinationPath, relativePath));
		},
		{
			class: "icon-folder-open"
		});
	ui.loadingEnd();
}


/**
 * Import sheet into the current project
 * @param {String} paths - Path to the sheet file / folder
 * @param {Number} [columns=1] - Index of the destination column to put the translation into
 * @param {Object} options 
 * @param {String} [options.sourceColumn=auto] - The source column
 * @param {Boolean} [options.overwrite=false] - When True will overwrite the existing value
 * @param {String[]} options.files - List of targeted files 
 * @param {Number} [options.sourceKeyColumn=0] - Column index of the translation's key
 * @param {Number} [options.keyColumn=0] - Key column of the current project
 * @param {Boolean} options.stripCarriageReturn - Whether to strip the carriage retrun character or not 
 */
 Trans.prototype.importSheet = async function(paths, columns, options) {
	// import sheets from a folder or file
	columns = columns||1; // target Column
	options = options||{};
	options.sourceColumn 	= options.sourceColumn||"auto";
	options.overwrite 		= options.overwrite||false;
	options.files 			= options.files||trans.getCheckedFiles()||[];
	options.sourceKeyColumn = options.sourceKeyColumn||0;
	options.keyColumn 		= options.keyColumn||0;
	options.newLine 		= options.newLine||undefined;
	options.stripCarriageReturn = options.stripCarriageReturn||false;
	options.ignoreNewLine 	= true; // let's set to true;

	console.log("trans.importSheet");
	console.log(arguments);
	//return console.log("halted");
	
	if (Array.isArray(paths) == false) paths=[paths];
	
	ui.showLoading();

	ui.loadingProgress(0, t("Collecting paths"), {consoleOnly:true, mode:'consoleOutput'});

	ui.log("Building indexes")
	if (options.files?.length == 0) {
		options.files = this.getAllFiles();
	}
	options.indexes = this.buildIndexes(options.files, false);
	
	var allPaths = [];
	
	for (let i=0; i<paths.length; i++) {
		var path = paths[i];
		if (common.isExist(path) == false) {
			ui.loadingProgress(0, t("Path : '")+path+t("' doesn't exist"), {consoleOnly:true, mode:'consoleOutput'});
			console.log("Error, path not exist"); 
			continue;
		}
		
		if (common.isDir(path)) {
			var dirTree = common.dirContentSync(path);
			allPaths = allPaths.concat(dirTree);
		} else {
			allPaths.push(path);
		}
	}
	ui.loadingProgress(0, allPaths.length+t(" file(s) collected!"), {consoleOnly:true, mode:'consoleOutput'});


	ui.loadingProgress(0, t("Start importing data!"), {consoleOnly:true, mode:'consoleOutput'});
	var processFile;
	if (common.parseSheet) {
		ui.log("Import using sheet parser add-on")
		processFile = async function(filePath) {
			var data = await common.parseSheet(filePath);
			var merged = [];
			for (var i in data) {
				console.log("Merging data", data[i]);
				
				if (!data[i]) continue;
				if (!data[i].length) continue;
				merged = merged.concat(data[i]);
			}
			console.log("output data :");
			console.log(merged);
			trans.translateFromArray(merged, columns, options);				
		}
	} else {
		ui.log("Import using legacy sheet importer")
		processFile = function(filePath) {
			filePath = filePath||path;
			php.spawnSync("import.php", {
				args:{
					//'path':'F:\\test\\export',
					'path':filePath,
					output:null,
					mergeSheet:true,
					prettyPrint:true
				},
				onDone : function(data) {
					console.log("output data :");
					console.log(data);
					trans.translateFromArray(data, columns, options);				
				}
			});		
			
		}
	}

	
	for (let i=0; i<allPaths.length; i++) {
		var thisFile = allPaths[i];
		ui.loadingProgress(Math.round(i/allPaths.length*100), t("Importing : ")+thisFile, {consoleOnly:true, mode:'consoleOutput'});
		await processFile(thisFile);
		ui.loadingProgress(Math.round((i+1)/allPaths.length*100), t("Done!"), {consoleOnly:true, mode:'consoleOutput'});		
	}
	
	trans.refreshGrid();
	trans.evalTranslationProgress();
	ui.loadingProgress(t("Done!"), t("All Done!"), {consoleOnly:true, mode:'consoleOutput'});
	ui.loadingEnd();

}

/**
 * Import translation from RPGMTransPatch
 * @param {String} paths - Path to the sheet file / folder
 * @param {Number} [columns=1] - Index of the destination column to put the translation into
 * @param {Object} options 
 * @param {String} [options.sourceColumn=auto] - The source column
 * @param {Boolean} [options.overwrite=false] - When True will overwrite the existing value
 * @param {String[]} options.files - List of targeted files 
 * @param {Number} [options.keyColumn=0] - Key column of the current project
 * @param {Boolean} options.stripCarriageReturn - Whether to strip the carriage retrun character or not 
 */
 Trans.prototype.importRPGMTrans = function(paths, columns, options) {
	// Import translation from RPGMTransPatch
	columns 			= columns||1; // target Column
	options 			= options||{};
	options.sourceColumn = options.sourceColumn||"auto";
	options.overwrite 	= options.overwrite||false;
	options.files 		= options.files||trans.getCheckedFiles()||[];
	options.sourceKeyColumn = options.sourceKeyColumn||0;
	options.keyColumn 	= options.keyColumn||0;
	options.newLine		= options.newLine||undefined;
	options.stripCarriageReturn = options.stripCarriageReturn||false;
	options.ignoreNewLine = true; // let's set to true;	
	console.log("trans.importRPGMTrans");
	console.log(arguments);
	//return console.log("halted");
	
	if (Array.isArray(paths) == false) paths=[paths];
	
	ui.showLoading();

	ui.loadingProgress(0, t("Collecting paths"), {consoleOnly:true, mode:'consoleOutput'});

	
	var allPaths = [];
	for (var i=0; i<paths.length; i++) {
		var path = paths[i];
		if (common.isExist(path) == false) {
			ui.loadingProgress(0, t("Path : '")+path+t("' doesn't exist"), {consoleOnly:true, mode:'consoleOutput'});
			console.log("Error, path not exist"); 
			continue;
		}
		
		if (common.isDir(path)) {
			var dirTree = common.dirContentSync(path);
			allPaths = allPaths.concat(dirTree);
		} else {
			allPaths.push(path);
		}
	}
	ui.loadingProgress(0, allPaths.length+t(" file(s) collected!"), {consoleOnly:true, mode:'consoleOutput'});


	ui.loadingProgress(0, t("Start importing data!"), {consoleOnly:true, mode:'consoleOutput'});

	
	var processFile = function(filePath) {
		filePath = filePath||path;
		php.spawnSync("parseTrans.php", {
			args:{
				'path':filePath,
				prettyPrint:true
			},
			onDone : function(data) {
				console.log("output data :");
				console.log(data);
				if (Array.isArray(data.data)) {
					trans.translateFromArray(data.data, columns, options);			
				}				
			}
		});
	}
	
	for (let i=0; i<allPaths.length; i++) {
		var thisFile = allPaths[i];
		ui.loadingProgress(Math.round(i/allPaths.length*100), t("Importing : ")+thisFile, {consoleOnly:true, mode:'consoleOutput'});
		processFile(thisFile);
		ui.loadingProgress(Math.round((i+1)/allPaths.length*100), t("Done!"), {consoleOnly:true, mode:'consoleOutput'});		
	}
	
	trans.refreshGrid();
	trans.evalTranslationProgress();
	ui.loadingProgress(t("Done!"), t("All Done!"), {consoleOnly:true, mode:'consoleOutput'});
	ui.loadingEnd();
	
}




 

// ===============================================================
// STATUS BAR
//================================================================

/**
 * Clear the status bar
 */
 Trans.prototype.clearFooter = function() {
	$(".footer .footer1 span").html("")
	$(".footer .footer2 span").html("")
	$(".footer .footer3 span").html("")
	$(".footer .footer4 span").html("")
	$(".footer .footer5 span").html("")
}

/**
 * Set the value of the current context into the status menu
 * @param {Number} row - Selected row 
 */
 Trans.prototype.setStatusBarContext = function(row) {
	if (typeof row== 'undefined') {
		if (Array.isArray(trans.grid.getSelected())) row = trans.grid.getSelected()[0][0];
	}
	
	//if (typeof row == 'undefined') return false;
	//if (typeof trans.project == 'undefined') return false;
	//console.log(trans.project.files[trans.getSelectedId()].context[row]);

	var currentId = trans.getSelectedId();
	try {
		if (trans.project.files[currentId].originalFormat == '> ANTI TES PATCH FILE VERSION 0.2' && this.project.parser !== "rmrgss") {
			//$(".footer .footer1>span").html(currentId+"/"+trans.buildContextFromParameter(trans.project.files[currentId].parameters[row]));
			$(".footer .footer1>span").html(trans.buildContextFromParameter(trans.project.files[currentId].parameters[row]));
		} else {
			//$(".footer .footer1>span").html(currentId+"/"+trans.project.files[currentId].context[row]);
			$(".footer .footer1>span").html(trans.project.files[currentId].context[row].join("; "));
		}
		
		$(".footer .footer1>span").addClass("icon-th-2")
	}
	catch(err) {
		$(".footer .footer1>span").html("");
		$(".footer .footer1>span").removeClass("icon-th-2")
		
	}
}

/**
 * Set the Number of row section of the status bar
 * @returns {Boolean} False on fail
 */
 Trans.prototype.setStatusBarNumData = function() {
	if (typeof trans.project == 'undefined') return false;
	try {
		$(".footer .footer3 span").html("rows : "+trans.project.files[trans.getSelectedId()].data.length);
	}
	catch(err) {
		$(".footer .footer3 span").html("");
	}
}

/**
 * Set the engine information section of the status bar
 */
 Trans.prototype.setStatusBarEngine= function() {
	if (typeof trans.project == 'undefined') return false;
	try {
		var parser = "";
		if (trans.project.parser) parser = `/<span title='parser'>${trans.project.parser}</span>`
		$(".footer .footer4 span").html(trans.project.gameEngine+parser);
	}
	catch(err) {
		$(".footer .footer4 span").html("");
	}
}

/**
 * 
 * @param {String} type  - Type of the icon (notice, warning, notice, translatorPlusPlus)
 */
 Trans.prototype.setTrayIcon = function(type) {
	// type : notice, warning, notice, translatorPlusPlus
	type = type || "translatorPlusPlus";
		var $icon = $('<i class="icon trayIcon"></i>');
		$icon.addClass(type);
		$(".footer .footer5 span").html($icon);
}


//================================================================
//
// EDITOR SECTION PART
//
//================================================================
/**
 * Go to the botom most part of the grid to the new key section
 */
 Trans.prototype.goToNewKey = function() {
	if (Array.isArray(this.data) == false) return false;
	this.grid.scrollViewportTo(this.data.length-1);
	this.grid.selectCell(this.data.length-1,0);
	//if ($(document.activeElement).is("#currentCellText"));
	
}

/**
 * Clear text editor. Bottom right editor.
 * @returns {Boolean} True if success
 */
 Trans.prototype.clearEditor = function() {
	const $cellText = $("#currentCellText");
	$cellText.val("");
	$cellText.prop("readonly", true);
	$cellText.data("column", null);
	$cellText.data("row", null);
	return true;
}

/**
 * Clear the Current Cell info section
 * @returns {Boolean} True if success
 */
 Trans.prototype.clearCellInfo = function() {
	const $cellInfo = $("#currentCoordinate");
	$cellInfo.val("");
	return true;
}

/**
 * Set value of the Current Cell info section
 * @param {Number} row 
 * @param {Number} column 
 */
Trans.prototype.setCellInfo = function(row, column) {
	const $cellInfo = $("#currentCoordinate");
	var drawedRow = row+1;
	var drawedCol = column+1;
	$cellInfo.val(drawedRow+","+drawedCol);
	trans.lastSelectedCell = [row, column];
}

Trans.prototype.drawCellEmblem = function() {
	const row = this.lastSelectedCell[0]
	const col = this.lastSelectedCell[1]
	$(".cellEmblems > i").addClass("hidden");
	if (col == this.keyColumn) return;

	$(".cellEmblems > .emblemComment").attr("title", "");
	if (this.isOrganicCell(row, col)) $(".cellEmblems > .emblemOrganic").removeClass("hidden")

	if (this.isVisitedCell(row, col)) {
		$(".cellEmblems > .emblemFootprint").removeClass("hidden")
	} else {
		$(".cellEmblems > .emblemFirstVisit").removeClass("hidden")
	}
	let comment = this.getCellComment(row, col);
	if (comment) {
		$(".cellEmblems > .emblemComment").removeClass("hidden")
		$(".cellEmblems > .emblemComment").attr("title", "<b>Cell comment:</b><br />"+comment);
	}
}

/**
 * connect this.data to trans.project.files[trans.getSelectedId()].data
 */
 Trans.prototype.connectData = function() {
	// connect this.data to trans.project.files[trans.getSelectedId()].data
	if (!this.getSelectedId()) return console.warn("unable to connect data with selected id");
	if (!this.project.files[this.getSelectedId()].data) return console.warn("unable to connect data with selected id");
	this.project.files[this.getSelectedId()].data = trans.data;
}

/**
 * Check whether row is the last row
 * @param {Number} row - Row index to be checked
 * @returns {Boolean} True if row is the last row
 */
 Trans.prototype.isLastRow = function(row) {
	//trans.data = trans.data || [];
	
	if (Boolean(trans.data) == false) {
		trans.data = [];
		trans.connectData();
	}
	
	row = row || 0;
	if (row == trans.data.length-1) return true;
	return false;
}

/**
 * Get the currently active Table
 * @returns {String[][]} Two dimensional array of the data
 */
 Trans.prototype.getCurrentData = function() {
	return this.data;
}

/**
 * Get text from the last selected cell
 * @returns {String} Text of the last selected cell
 */
 Trans.prototype.getTextFromLastSelected = function() {
	var data = this.getCurrentData();
	if (empty(this.lastSelectedCell)) return "";
	return data[this.lastSelectedCell[0]][this.lastSelectedCell[1]];

}

/**
 * Get the value of the Text Editor field
 * @param {String} text Value of the Text Editor field
 * @param {Boolean} [triggerEvent=false] - Trigger change event
 */
 Trans.prototype.textEditorSetValue = function(text, triggerEvent=false) {
	$("#currentCellText").val(text);
	ui.generateBackgroundNumber();
	if (triggerEvent) $("#currentCellText").trigger("change")
}

/**
 * Standard procedure executed after selecting cells
 * @param {Number} row - Row from
 * @param {Number} column - Column from
 * @param {Number} row2 - Row to
 * @param {Number} column2 - Column to
 */
 Trans.prototype.doAfterSelection = function(row, column, row2, column2) {
	const $editor = $("#currentCellText");
	
	//$editor.val(this.getValue());
	//$editor.val(trans.grid.getCellMeta(row,column).instance.getValue());
	$editor.val(trans.data[row][column]);
	$editor.prop("readonly", false);
	
	var isLastRow = trans.isLastRow(row)
	if (column == 0) {
		if ( isLastRow == false) {
			$editor.prop("readonly", true);
		}
	}
	$editor.data("column", column);
	$editor.data("row", row);
	trans.setCellInfo(row, column);
	trans.setStatusBarContext(row);
	
	trans.translateSelectedRow(row);
	ui.generateBackgroundNumber($editor);
	if (typeof romaji !== 'undefined') {
		if (trans.config.loadRomaji == false ) return true;
		romaji.resolve(trans.data[row][0], $("#currentRomaji .text"));
	}

	// romaji header
	$("#currentRomaji .header").text(this.getRowInfoText(row, true) || "");

	this.drawCellEmblem();
	
	// leave trail with debounce
	if (this.getOption("gridInfo")?.isRuleActive && this.getOption("gridInfo")?.enableTrail) {
		if (this._cellInfoTrack) clearTimeout(this._cellInfoTrack);
		if (column != this.keyColumn && this.getText(row, column)) {
			this._cellInfoTrack = setTimeout(()=> {
				clearTimeout(this._cellInfoTrack);
				this._cellInfoTrack = undefined;
				console.log("Setting cellInfo", "v", 1, this.getSelectedId(), row, column);
				this.cellInfo.set("v", 1, this.getSelectedId(), row, column);
				$(`table tbody td[data-coord="${row}-${column}"]`).addClass("viewed")
			}, 1000)
		}
	}



	const thisObj = this.getSelectedObject();
	thisObj.lastSelectedCell = [row, column];
	/**
	 * Trigger event right after a cell(s) is selected
	 * @event Trans#onAfterSelectCell
	 * @param  {Object} Options
	 * @param  {Number} Options.fromRow
	 * @param  {Number} Options.fromCol
	 * @param  {Number} Options.toRow
	 * @param  {Number} Options.toCol
	 * @param  {Boolean} Options.isLastRow
	 */
	this.trigger("onAfterSelectCell", 
		{
			fromRow:row, fromCol:column, toRow:row2, toCol:column2, isLastRow:isLastRow
		}
	);
}


//================================================================
//
// HANDLING FILE NAVIGATION
//
//================================================================

/**
 * Resets current cell editor
 */
 Trans.prototype.resetCurentCellEditor = function() {
	var $currentCellText = $("#currentCellText");
	$currentCellText.val("");
	$currentCellText.data("row", 0)
	$currentCellText.data("column", 0)
	trans.setCellInfo(0, 0);
	trans.setStatusBarContext(0);	
	$("#currentRomaji .text").text("");	
	$("#currentRomaji .header").text("");	
}

/**
 * Creates a new file(new object), register it into the left panel
 * @param {String} filename - Name of the file
 * @param {String} dirname - Directory location
 * @param {Object} options 
 * @param {String} options.originalFormat - Original format of the file
 * @param {String} options.type - File type
 * @returns {Object}
 */
 Trans.prototype.createFile = function(filename, dirname, options) {
	// Create a new file
	// register it into the left panel
	options = options || {};
	options.originalFormat 	= options.originalFormat || ""
	options.type 			= options.type || null

	var isValid = require('is-valid-path');	
	dirname = dirname || "/"
	if (!isValid(filename)) {
		return {
			error:true,
			msg : filename+ t(" is not a valid object name")
		}
	}
	
	if (!isValid(dirname) && dirname !== "*") {
		return {
			error:true,
			msg : dirname+ t(" is not a valid directory name")
		}
	}
	
	var fullPath = nwPath.join("/", dirname, filename);
	var fileObj;

	if (this.getObjectById(fullPath)) {
		return {
			error:true,
			msg : fullPath+ t(" is already exist")
		}
	}

	if (dirname == "*") {
		// create a reference
		fullPath = filename;
		options.type = options.type || "reference"
		fileObj = this.createFileData(fullPath, {
				originalFormat 	: options.originalFormat || "TRANSLATOR++ GENERATED TABLE",
				type			: options.type,
				dirname			: "*"
			});
	} else {
		fullPath = fullPath.replace(/\\/g, "/");
		fileObj = this.createFileData(fullPath, {
				originalFormat 	: options.originalFormat,
				type			: options.type
			});
	}
	


	trans.project.files[fullPath] = fileObj;
	
	trans.addFileItem(fullPath, fileObj);
	this.evalTranslationProgress();
	ui.fileList.reIndex();
	ui.initFileSelectorDragSelect();	
	return {}
}

/**
 * Select of 
 * @param {JQuery} $element - Selected element
 * @param {Object} options 
 * @param {Function} options.onDone - When selected done
 * @returns {JQuery} - Instance of jquery of the selected element
 */
 Trans.prototype.selectFile = function($element, options) {
	options = options||{};
	options.onDone = options.onDone||undefined;
	this.grid.deselectCell()
	if (typeof $element == "string") $element = $(".fileList [data-id="+common.escapeSelector($element)+"]");
	const thisID = $element.closest("li").data("id");
	this.trigger("beforeSelectFile", [trans?.project?.selectedId, thisID])

	//console.log("switching to other file");
	$element.closest(".tree").find("li").removeClass("selected");
	$element.closest("li").addClass("selected");

	trans.project.selectedId 	= thisID;
	trans.data 					= trans.project.files[thisID].data;
	//trans.indexIds			= trans.project.files[thisID].indexIds;
	trans.selectedData 			= trans.project.files[thisID];
	// force reindexig each build
	trans.buildIndex();
	trans.clearCellInfo();
	trans.clearEditor();
	trans.setStatusBarNumData();
	trans.resetCurentCellEditor();

	if ($(".menu-button.addNote").hasClass("checked")) {
		ui.openFileNote();
	} 
	$(".fileId").val(thisID);

	ui.disableGrid(false);
	ui.evalFileNoteIcon();
	this.trigger("objectSelected", thisID);
	this.grid.loadData(trans.data);
	//trans.loadComments();
	//trans.refreshGrid({onDone:options.onDone});
	//trans.grid.render();
	trans.loadComments();
	trans.renderGridInfo();

	trans.grid.setFixedTableHeightByData(trans.data)
	trans.grid.scrollViewportTo(0, 0);

	// const thisObjLastSelectedCell = trans.project?.files?.[thisID]?.lastSelectedCell;
	// if (Array.isArray(thisObjLastSelectedCell)) {
	// 	trans.grid.scrollViewportTo(thisObjLastSelectedCell[0], thisObjLastSelectedCell[1]);
	// 	trans.grid.selectCell(thisObjLastSelectedCell[0], thisObjLastSelectedCell[1]);
	// }
	return $element;
	
}

Trans.prototype.renderGridInfo = function() {
	// render options in trans.project.options.gridInfo
	let gridInfo = this.getOption("gridInfo") || {};
	if (gridInfo?.isRuleActive && gridInfo?.rowHeaderInfo) {
		let rowHeaderWidth = gridInfo.rowHeaderWidth||130
		trans.grid.updateSettings({
			rowHeaderWidth: rowHeaderWidth
		});
		$("#table").css("--row-header-width", rowHeaderWidth+"px")
	} else {
		trans.grid.updateSettings({
			rowHeaderWidth: null
		})
		const getWidth = $(`#table [data-role="tablecorner"]`).outerWidth();
		$("#table").css("--row-header-width", getWidth+"px")
	}
}

/**
 * Add a new filegroup
 * @param {String} dirname - name of the group
 * @returns {Boolean} True if success
 */
 Trans.prototype.addFileGroup = function(dirname, fileObj) {
	var $group = $("#fileList [data-group='"+CSS.escape(dirname)+"']");
	//console.log("Group : ", dirname , $group.length);
	if ($group.length < 1) {
		//console.log("creating new header");
		var hTemplate = $("<li class='group-header'  data-group='"+dirname+"'>"+dirname+"</li>");
		
		if ($("#fileList .fileListUl .group-header[data-group='*']").length > 0) {
			$("#fileList .fileListUl .group-header[data-group='*']").before(hTemplate);
		} else {
			$("#fileList .fileListUl").append(hTemplate);
		}
		return true;
	}
	return false;
}

/**
 * Check whether the ui element of the file is exist or not
 * @param {String} file - File id
 * @param {Object} fileObj - File object
 * @returns {Boolean} True if exist
 */
 Trans.prototype.fileItemExist = function(file, fileObj) {
	if ($("#fileList [data-group='"+CSS.escape(fileObj.dirname)+"'][data-id='"+CSS.escape(file)+"']").length>0) {
		return true;
	}
	return false;
}

/**
 * Draw file status
 * @param {String[]} files - list of file ID
 */
 Trans.prototype.drawFileStatus = function(files) {
	if (typeof files == "string") files = [files];
	var $container = $("#fileList");
	for (var i=0; i<files.length; i++) {
		(()=> {
			var thisFile 	= files[i];
			var thisObj 	= this.getObjectById(thisFile);
			var $li 	= $container.find(`[data-id="${CSS.escape(files[i])}"]`);
			if ($li.length == 0) return;
	
			$li.removeClass("isCompleted");
			$li.removeClass("isRequireAttention");
			if (thisObj.isCompleted) $li.addClass("isCompleted");
			if (thisObj.isRequireAttention) $li.addClass("isRequireAttention");
	
			// handling note
			//$markersWrapper.empty();
			if ($li.data("noteTooltipIsActive")) {
				$li.tooltip("destroy");
				$li.data("noteTooltipIsActive", false);
			}

			var $markersWrapper = $li.find(".markers");
			$markersWrapper.empty();
			if (thisObj.note) {
				// add keyword
				$markersWrapper.append($(`<span class="hidden"></span>`).text(thisObj.note))

				var $noteIcon = $(`<i class="icon-commenting"></i>`);
				$markersWrapper.append($noteIcon);
				
				if (thisObj.noteColor) $noteIcon.css("color", thisObj.noteColor);

				// for some reason I can not hook the mouse events on $noteIcon
				// so I hook it into $li instead. This behavior is acceptable for now.
				$li.tooltip({
					content: function() {
						var $tooltip = $("<div class='fileObjTooltipWindow'></div>");
						if (thisObj.noteColor) {
							$tooltip.css("border-left-color", thisObj.noteColor);
							$tooltip.addClass("hasColor")
						}
						$tooltip.text(thisObj.note);
						$tooltip.on("mouseenter", function() {
							ui.fileObjectTooltip.open($tooltip.clone(), $li)
						})
						return $tooltip;
					},
					tooltipClass: "fileObjTooltipWrapper",
					show: { 
						effect: "fade", 
						duration: 100 
					},
					hide: {
						effect: "none",
						delay: 100
					},
					position: {
						my: "left top",
						at: "right+6 top-16",
						of: $li
					},
					open: function( event, ui ) {
	
					}
				});

				$li.data("noteTooltipIsActive", true);

			}
		})()
	}
}

/**
 * Add file item into the left panel
 * @param {String} file - File ID
 * @param {fileObj} fileObj - File Object
 * @returns {Boolean}
 */
 Trans.prototype.addFileItem = function(file, fileObj) {
		// skip if exist
		if (this.fileItemExist(file, fileObj)) return false;
		// draw header if exist
		this.addFileGroup(fileObj.dirname);

		var $li = $("<li></li>");
		$li.append("<input type='checkbox' class='fileCheckbox' title='hold shift for bulk selection' />"+
			"<a href='#' class='filterable'><span class='filename'></span>"+
				"<span class='markers'></span>"+
				"<span class='percent' title='progress'></span>"+
				"<div class='progress' title='progress'></div>"+
			"</a>");
		$li.attr("title", file);
		$li.attr("data-group", fileObj.dirname);
		$li.find(".fileCheckbox").attr("value", file);
		$li.find(".filename").text(fileObj.filename);

		$li.addClass("data-selector");
		$li.data("id", file);
		$li.attr("data-id", file);
		//$li.data("data", fileObj);

		$li.find("a").on("mousedown", function(e) {
			//console.log("middle click clicked");
			//console.log(e);
			if( e.which == 2 ) {
				e.preventDefault();
				trans.clearSelection();
				return false;
			}			
		});
		$li.find("a").on("dblclick", function(e) {
			// select that item
			var $thisCheckbox = $(this).closest("li").find(".fileCheckbox");
			$thisCheckbox.prop("checked", !$thisCheckbox.prop("checked")).trigger("change")
			
		});
		$li.find("a").on("click", function(e) {
			//console.log("clicked");
			e.preventDefault();
			trans.selectFile($(this).closest("li"));
		});
		$li.find(".fileCheckbox").on("change", function() {
			if ($(this).prop("checked") == true) {
				trans.$lastCheckedFile = $(this);
				$(this).closest(".data-selector").addClass("hasCheck");
			} else {
				trans.$lastCheckedFile = undefined;
				$(this).closest(".data-selector").removeClass("hasCheck");
			}
		});
		$li.find(".fileCheckbox").on("mousedown", function(e) {
			if (!trans.$lastCheckedFile) return false;
			if (e.shiftKey) {
				console.log("The SHIFT key was pressed!");
				var $checkBoxes = $(".fileList .fileCheckbox");
				var lastIndex = $checkBoxes.index(trans.$lastCheckedFile);
				var thisIndex = $checkBoxes.index(this);
				var chckFrom;
				var chckTo;
				if (lastIndex < thisIndex) {
					chckFrom = lastIndex;
					chckTo = thisIndex;
				} else {
					chckFrom = thisIndex;
					chckTo = lastIndex;
				}
				console.log("check from index "+chckFrom+" to "+chckTo);
				for (var i=chckFrom; i<chckTo; i++) {
					$checkBoxes.eq(i).prop("checked", true).trigger("change");
				}
				
			} 		
		});

		
		$("#fileList [data-group='"+CSS.escape(fileObj.dirname)+"']").last().after($li);
	
}

/**
 * Draw the left panel
 * @fires Trans#onLoadTrans
 */
 Trans.prototype.drawFileSelector = function() {
	if (typeof this.project.files == 'undefined') return false;
	$("#fileList .fileListUl").empty();	
	this.generateNewDictionaryTable();
	
	for (var file in this.project.files) {
		this.addFileItem(file, this.project.files[file]);
	}
	this.drawFileStatus(this.getAllFiles());
	this.setStatusBarEngine();
	this.evalTranslationProgress();
	ui.fileList.reIndex();
	ui.initFileSelectorDragSelect();
	ui.enableButtons();
	this.renderGridInfo();
	var TranslationByContext = require("www/js/TranslationByContext.js");
	ui.translationByContext = new TranslationByContext();
	//engines.handler('onLoadTrans').apply(this, arguments);
	this.inProject = true;
	engines.current().triggerHandler('onLoadTrans', this, arguments);
	
	this.initLocalStorage();
	/**
	 * Trigger event after trans file is loaded
	 * @event Trans#onLoadTrans
	 */
	this.trigger('onLoadTrans');

}

Trans.prototype.initLocalStorage = async function() {
	let thisProjectDB = trans?.project?.projectId || "global"
	this.localStorage = new (require("better-localstorage"))("tp"+thisProjectDB);
}

Trans.prototype.unInitLocalStorage = async function() {
	if (typeof this.localStorage?.db?.close == "function") await this.localStorage.db.close();
	this.localStorage = undefined;
}

/**
 * Mark a file as complete
 * @param {Boolean} mark - True if complete
 * @param {String[]} [files=this.getAllFiles()] - List of the file IDs
 * @returns {Boolean}
 */
 Trans.prototype.setMarkAsComplete = function(mark, files) {
	files = files || trans.getCheckedFiles();
	if (files.length < 1) files = trans.getAllFiles();
	for (var i=0; i<files.length; i++) {
		var thisFile = files[i];
		this.getObjectById(thisFile).isCompleted = mark;
	}
	this.drawFileStatus(files);
	return true;
}

/**
 * select all with matching filter
 * @param {(String|String[])} [filter=All] - List of the selected file ID. When empty then all will be selected 
 * @param {Boolean} append - If true, then add into the previous selection
 */
 Trans.prototype.selectAll = function(filter, append) {
	filter = filter||[];
	append = append||false;
	if (typeof filter == 'string') filter = [filter];
	
	var $checkBoxes = $("#fileList .fileCheckbox");
	
	if (filter.length == 0) {
		$checkBoxes.each(function() {
			$(this).prop("checked", true).trigger("change");
		});
	} else {
		if (!append) $checkBoxes.prop("checked", false).trigger("change");
		$checkBoxes.each(function() {
			var $this = $(this);
			if (filter.includes($this.closest("li").data("id"))) $this.prop("checked", true).trigger("change");
		});
	}
}

/**
 * Inverts current selection
 */
 Trans.prototype.invertSelection = function() {
	var $checkBoxes = $("#fileList .fileCheckbox");
	$checkBoxes.each(function() {
		$(this).prop("checked", !$(this).prop("checked")).trigger("change");
	});
}

/**
 * Clear current selection
 */
 Trans.prototype.clearSelection = function() {
	var $checkBoxes = $("#fileList .fileCheckbox");
	$checkBoxes.each(function() {
		$(this).prop("checked", false).trigger("change");
	});
}

/**
 * Initialize file navigator
 */
 Trans.prototype.initFileNav = function() {	
	//console.log(trans.fileListLoaded);
	//this function will be executed whenever initializing a new trans file
	//this function is suitable to hook all initialization event of trans
	console.log("running trans.initFileNav");
	//console.log("current trans : ", trans);
	
	// reevaluating trans.fileListLoaded based on existance of trans.project.files
	try {
		if (typeof trans.project.files !=='undefined') {
			trans.fileListLoaded = true;
		} else {
			trans.fileListLoaded = false;
		}
	} catch (e) {
		trans.fileListLoaded = false;
	}
	
	if (trans.fileListLoaded == false) {
		trans.createProject({
			onAfterLoading:function() {
				trans.drawFileSelector();
			}
		});

		return false;
	} else {
		this.unInitFileNav();
		trans.drawFileSelector();
		this.onFileNavLoaded.call(this);
		//engines.handler('onLoadTrans').apply(this, arguments);
		
		/**
		 * Triggers after trans loaded
		 * @event Trans#transLoaded
		 * @param  {Trans} this - Instance of trans
		 */
		this.trigger("transLoaded", this);
	}
}

/**
 * Un-initialize file navigator
 * @fires Trans#onUnloadTrans
 * @fires Engines#onUnloadTrans
 */
 Trans.prototype.unInitFileNav = function() {
	$("#fileList .fileListUl").empty();
	ui.fileList.reIndex();
	this.onFileNavUnloaded.call(this);
	engines.handler('onUnloadTrans').apply(this, arguments);
	ui.ribbonMenu.clear();
	ui.disableButtons();
	/**
	 * Triggers after a project is closed.
	 * @event Trans#onUnloadTrans
	 */
	this.trigger('onUnloadTrans')
}

/**
 * Evaluate translation progress
 * @param {(String|String[])} [file=All] - List of file IDs to be evaluated
 * @param {Object} [progressData] - Progress data 
 */
 Trans.prototype.evalTranslationProgress = function(file, progressData) {
	//data = data||{};
	file = file||[];
	var dataResult = progressData||trans.countTranslated(file)||{};
	
	//if (typeof data[file] == 'undefined') dataResult = trans.countTranslated(file);
	
	for (var id in dataResult) {
		var fileSelector = $(".fileList [data-id="+common.escapeSelector(id)+"]");
		fileSelector.find(".percent").text(Math.round(dataResult[id].percent));
		fileSelector.find(".progress").css("background", "linear-gradient(to right, #3159f9 0%,#3159f9 "+dataResult[id].percent+"%,#ff0004 "+dataResult[id].percent+"%,#ff0004 100%)");
	}
}

/**
 * Get information of the current progress
 * @param {Boolean} reset - If True, will reset the stats
 * @returns {Object} Stats of the project
 */
 Trans.prototype.getStats = function(reset) {
	function countWords(str) {
		return str.trim().split(/\s+/).length;
	}
	var stats = {
		files : 0,
		folders: 0,
		progress: 0,
		words:0,
		characters:0,
		rows:0,
		rowTranslated:0,
		percent:0,
		organic:0
	}
	var fromCache = false;
	if (!this.project) return stats;
	if (!this.project.files) return stats;
	if (!reset) {
		if (this.project.stats) {
			fromCache = true;
			stats = this.project.stats;
		}
	}

	for (var i in this.project.files) {
		var thisObj = this.project.files[i];
		if (thisObj.originalFormat == "TRANSLATOR++ GENERATED TABLE") continue;
		stats.files++;

		if (thisObj.progress) {
			stats.rows 				+= thisObj.progress.length;
			stats.rowTranslated 	+= thisObj.progress.translated;
		}

		if (fromCache) continue;
		if (empty(thisObj.data)) continue;
		for (var row=0; row<thisObj.data.length; row++) {
			var thisRow = thisObj.data[row];
			if (empty(thisRow)) continue;
			if (!thisRow[this.keyColumn]) continue;
			stats.characters 	+= thisRow[this.keyColumn].length;
			stats.words 		+= countWords(thisRow[this.keyColumn]);

			// calculating organic
			var translator = this.cellInfo.getBestCellInfo(i, row, "t");
			if (translator == "HU") stats.organic++
		}
	}
	if (stats.rows > 0) stats.percent  = (stats.rowTranslated/stats.rows) * 100;
	stats.folders 	= $(".fileList  .group-header").length - 1;
	stats.organicPercent = (stats.organic/stats.rowTranslated) *100;

	this.stats = stats;
	return stats;
}

Trans.prototype.setFileNoteColor = function(color, files) {
	files ||= this.getSelectedId();
	if (Array.isArray(files) == false) files = [files];
	for (let i in files) {
		let obj = this.getObjectById(files[i]);
		if (!color) {
			if (obj.noteColor) delete obj.noteColor;
			continue;
		}
		obj.noteColor = color;
	}

	this.drawFileStatus(files);
	ui.evalFileNoteIcon();
}

Trans.prototype.setFileNote = function(note, files) {
	files ||= this.getSelectedId();
	if (Array.isArray(files) == false) files = [files];
	for (let i in files) {
		let obj = this.getObjectById(files[i]);
		obj.note = note;
	}

	this.drawFileStatus(files);
	ui.evalFileNoteIcon();
	ui.fileList.reIndex();
}

/**
 * Load comments into the grid
 */
 Trans.prototype.loadComments = function() {
	trans.grid.comment = trans.grid.comment||trans.grid.getPlugin('comments');
	var selectedObj = trans.getSelectedObject();
	if (!selectedObj) return false;
	if (typeof selectedObj.comments == 'undefined') return false;
	
	for (var row in selectedObj.comments) {
		for (var col in selectedObj.comments[row]) {
			//console.log("set comment on", row, col, selectedObj.comments[row][col]);
			trans.grid.comment.setCommentAtCell(parseInt(row), parseInt(col), selectedObj.comments[row][col]);
		}
	}
}

// ===============================================================
// CONTEXT MENU
// ===============================================================

Trans.prototype.runCustomScript = async function(workspace, scriptPath, options) {
	console.log("Running custom script with arguments:", arguments);
	options = options || {};
	var CodeRunner = require("www/js/CodeRunner.js")
	var codeRunner = new CodeRunner();
	var code = await common.fileGetContents(scriptPath);

	if (!code) return alert(t("Error opening file :"+scriptPath))
	await ui.showBusyOverlay();
	await common.wait(200);
	try {
		var result = await codeRunner.run(code, workspace, options);
		if (result) alert(result);
	} catch (e) {
		alert(t("Error executing :")+nwPath.basename(scriptPath)+"\n"+e.toString());
	}
	await ui.hideBusyOverlay();
}

Trans.prototype.updateRunScriptMenu = function() {
	this.fileSelectorMenu = this.fileSelectorMenu || {};

	// rowByrow
	// resets menu
	this.fileSelectorMenu.withSelected.items.runScript.items.forEachRowRun.items = {};
	var forEachRowRunItems = this.fileSelectorMenu.withSelected.items.runScript.items.forEachRowRun.items;
	var rowIteratorConfig = sys.getConfig("codeEditor/rowIterator");
	if (!rowIteratorConfig) {
        sys.setConfig("codeEditor/rowIterator", {quickLaunch:[]});
        //sys.getConfig("codeEditor/rowIterator");
    }
	rowIteratorConfig ||= {}
    rowIteratorConfig.quickLaunch ||= [];
	for (let i=0; i<rowIteratorConfig["quickLaunch"].length; i++) {
		(()=>{
			var filePath = rowIteratorConfig["quickLaunch"][i];
			var filename = common.getFilename(filePath);
			var thisId = common.generateId()
			forEachRowRunItems[thisId] = {
				name: filename,
				callback: (key, opt) => {
					var conf = confirm(t("Are you sure want to execute the following script?")+"\n"+filename);
					if (!conf) return;
					this.runCustomScript("rowIterator", filePath);
				}
			}
		})()
	}


	// object by object
	// resets menu
	this.fileSelectorMenu.withSelected.items.runScript.items.forEachObjectRun.items = {};
	var forEachObjectRunItems = this.fileSelectorMenu.withSelected.items.runScript.items.forEachObjectRun.items;
	var objectIteratorConfig = sys.getConfig("codeEditor/objectIterator");
	if (!objectIteratorConfig) {
        sys.setConfig("codeEditor/objectIterator", {quickLaunch:[]});
        objectIteratorConfig = sys.getConfig("codeEditor/objectIterator");
    }
    objectIteratorConfig["quickLaunch"] = objectIteratorConfig["quickLaunch"] || [];

	for (let i=0; i<objectIteratorConfig["quickLaunch"].length; i++) {
		(()=>{
			var filePath = objectIteratorConfig["quickLaunch"][i];
			var filename = common.getFilename(filePath);
			var thisId = common.generateId()
			forEachObjectRunItems[thisId] = {
				name: filename,
				callback: (key, opt) => {
					var conf = confirm(t("Are you sure want to execute the following script?")+"\n"+filename);
					if (!conf) return;
					this.runCustomScript("objectIterator", filePath);
				}
			}
		})()
	}

}

Trans.prototype.updateRunScriptGridMenu = function() {
	// cell level
	this.gridContextMenu.runAutomation.submenu = {
		items: []
	}
	var forEachCellRunItems = this.gridContextMenu.runAutomation.submenu.items;
	var cellSelectionConfig = sys.getConfig("codeEditor/gridSelection");
	if (!cellSelectionConfig) {
		sys.setConfig("codeEditor/gridSelection", {quickLaunch:[]});
		cellSelectionConfig = sys.getConfig("codeEditor/gridSelection");
	}
	cellSelectionConfig["quickLaunch"] = cellSelectionConfig["quickLaunch"] || [];
	
	
	for (var i=0; i<cellSelectionConfig["quickLaunch"].length; i++) {
		(()=>{
			var filePath = cellSelectionConfig["quickLaunch"][i];
			console.log("Adding context menu", filePath);
			var filename = common.getFilename(filePath);
			var thisId = common.generateId()
			forEachCellRunItems.push({
				name: filename,
				key:"runAutomation:"+thisId,
				callback: (key, opt) => {
					var conf = confirm(t("Are you sure want to execute the following script?")+"\n"+filename);
					if (!conf) return;
					this.runCustomScript("gridSelection", filePath);
				}
			})
		})()
	}	
}

/**
 * Initialize grid's context menu
 * 
 */
 Trans.prototype.fileSelectorContextMenuInit = function() {
	console.log("trans.fileSelectorContextMenuInit");
	var trans = this;
	this.fileSelectorMenu = {
		"selectAll" : {"name" : t("Select all"), icon:"context-menu-icon icon-check2-all"},
		"clearSelection" : {"name" : t("Clear selection")},
		"selectCompleted" : {"name" : t("Select 100%")},
		"selectIncompleted" : {"name" : t("Select <100%")},
		"selectMarkedAsCompleted" : {"name" : t("Select completed")},
		"invertSelection" : {"name" : t("Invert selection")},
		"sep0": "---------",
		"markCompleteCurrent":{
			"name" : t("Toggle mark as complete"),
			icon: function() {
				return 'context-menu-icon icon-ok';
			}
		},
		"sep1": "---------",
		"withSelected": {
			name: () => {
				var checkedLength 	= $(".fileCheckbox:checked").length;
				if (checkedLength == 0) {
					
					trans.fileSelectorMenu.withSelected.icon = 'context-menu-icon icon-docs-1';
					trans.fileSelectorMenu.withSelected.items.deleteFiles.visible = false;
					return t("With all");
				} else {
					return t("With ") + checkedLength + t(" selected") 
				}
			},
			icon: function() {
				return 'context-menu-icon icon-check';
			},
			items: {
				"markComplete": {
					name : t("Mark as complete"),
					icon: 'context-menu-icon icon-ok'
				},
				"unsetMarkComplete": {
					name : t("Un-mark as complete")
				},
				"sep0-0":"---------",
				"batchTranslation": {
					name : t("Batch translation"),
					icon: 'context-menu-icon icon-language'
				},
				"sep0-1":"---------",
				"wrapText": {"name" : t("Wrap texts")},
				"trim": {"name" : t("Trim")},
				"padding": {"name" : t("Auto padding")},
				"createScript": {
					name : t("Create Automation"), 
					icon: 'context-menu-icon icon-code',
					items: {
						"forEachObject" : {"name": t("For each object"), icon: 'context-menu-icon icon-doc'},
						"forEachRow" : {"name": t("For each row"),icon: 'context-menu-icon icon-menu-1'},
					}
				},
				"runScript": {
					name : t("Run Automation"), 
					icon: 'context-menu-icon icon-play',
					items: {
						"forEachObjectRun" : {
							name: t("For each object"), 
							icon: 'context-menu-icon icon-doc',
							items: {

							}
						},
						"forEachRowRun" : {
							name: ()=> {
								console.log("rendering for each row");
								return t("For each row")
							},
							icon: 'context-menu-icon icon-menu-1',
							items: {
								
							}
						},
					}
				},
				"sep0-2":"---------",
				"import": {
					name:t("Import from..."),
					icon:"context-menu-icon icon-login",
					items: {
						"importFromTrans" : {"name": t("Trans File"),icon: 'context-menu-icon icon-tpp'},
						"importFromSheet" : {"name": t("Spreadsheets"),icon: 'context-menu-icon icon-file-excel'},
						"importFromRPGMTransPatch" : {"name": t("RPGMTransPatch Files"),icon: 'context-menu-icon icon-doc-text'}
					}
				},
				"export": {
					name:t("Export into..."),
					icon:"context-menu-icon icon-export",
					items: {
						"exportToGamePatch" : {"name": t("A folder"), icon:() => 'context-menu-icon icon-folder-add'},
						"exportToGamePatchZip" : {"name": t("Zipped Game Patch"),icon:() => 'context-menu-icon icon-file-archive'},
						"exportToCsv" : {"name": t("Comma Separated Value (csv)"),icon:() => 'context-menu-icon icon-file-excel'},
						"exportToXlsx" : {"name": t("Excel 2007 Spreadsheets (xlsx)"),icon:() => 'context-menu-icon icon-file-excel'},
						"exportToXls" : {"name": t("Excel Spreadsheets (xls)"),icon:() => 'context-menu-icon icon-file-excel'},
						"exportToOds" : {"name": t("ODS Spreadsheets"),icon:() => 'context-menu-icon icon-file-excel'},
						"exportToHtml" : {"name": t("Html Spreadsheets"),icon:() => 'context-menu-icon icon-file-code'},
						"exportToTransPatch" : {"name": t("RMTrans Patch"),icon:() => 'context-menu-icon icon-doc-text'}
					}
				},
				"inject" : {
					name: t("Inject Translation"),
					icon: "context-menu-icon icon-download"
				},
				"revert" : {
					name: t("Revert to original"),
					icon: "context-menu-icon icon-ccw"
				},
				"sep0-3":"---------",
				"clearTranslationSel": {"name" : t("Clear translation"), "icon":"context-menu-icon icon-eraser"},
				"deleteFiles": {"name" : t("Delete files"), "icon":"context-menu-icon icon-trash-empty"},

			}
		},
		"sep2": "---------",
		"properties": {
			name: t("Properties"), 
			icon:'context-menu-icon icon-cog'
		}
	}

	if (trans.fileSelectorContextMenuIsInitialized) return false;
	$.contextMenu({
		selector: '.fileList .data-selector', 
		events: {
			preShow : function($target, e) {
				//$(".context-menu-root").trigger("contextmenu:hide")
				/*
				console.log(arguments);
				var $cTarget = $target;
				$cTarget.closest("ul").find(".contextMenuOpened").removeClass("contextMenuOpened");
				$cTarget.addClass("contextMenuOpened");
				*/
				//console.log(arguments);
				
			},
			hide : function($target, e){
				//$(".fileList .data-selector.contextMenuOpened").removeClass("contextMenuOpened");

			}
		},			
		build: function($triggerElement, e) {
			var thisCallback = function(key, options) {
				switch (key) {
					case "selectAll" :
						trans.selectAll();
						break;
					case "clearSelection" :
						trans.clearSelection();
						break;
					case "invertSelection" :
						trans.invertSelection();
						break;
					case "selectCompleted" :
						trans.selectAll(trans.getAllCompletedFiles())
						break;
					case "selectIncompleted" :
						trans.selectAll(trans.getAllIncompletedFiles())
						break;
					case "selectMarkedAsCompleted" :
						trans.selectAll(trans.getAllMarkedAsCompleted());
						break;
					case "markCompleteCurrent" :
						var $elm = $("#fileList .context-menu-active");
						var action = !$elm.hasClass("isCompleted");
						var currentFile = $elm.data("id");
						trans.setMarkAsComplete(action, [currentFile]);
						break;
					case "markComplete" :
						trans.setMarkAsComplete(true);
						break;
					case "unsetMarkComplete" :
						trans.setMarkAsComplete(false);
						break;
					case "batchTranslation" :
						ui.translateAllDialog();
						break;
					case "forEachObject" :
						ui.openAutomationEditor("codeEditor_objectIterator", {
							workspace: "objectIterator"
						});
						break;
					case "forEachRow" :
						ui.openAutomationEditor("codeEditor_rowIterator", {
							workspace: "rowIterator"
						});
					break;
					case "clearTranslationSel" :
						var confirmation = confirm(t("Do you want to clear translation?"));
						var selection = trans.getCheckedFiles();
						if (confirmation) trans.removeAllTranslation(trans.getCheckedFiles(), {refreshGrid:true});
						trans.evalTranslationProgress(selection);
						break;
					case "clearTranslationAll" :
						var confirmation2 = confirm(t("Do you want to clear translation?"));
						var selection2 = trans.getAllFiles();
						if (confirmation2) trans.removeAllTranslation(trans.getAllFiles(), {refreshGrid:true});
						trans.evalTranslationProgress(selection2);
						break;
					case "deleteFiles" :
						ui.deleteFiles();
						break;
					case "wrapText" :
						ui.batchWrapingDialog();
						break;
					case "trim" :
						ui.openTrimWindow();
						break;
					case "padding" :
						ui.openPaddingWindow();
						break;
					case "properties" :
						ui.openFileProperties();
						break;
					// imports
					case "importFromSheet":
						ui.openImportSpreadsheetDialog();
						break;
					case "importFromTrans":
						$("#importTrans").trigger("click");
						break;
					case "importFromRPGMTransPatch":
						ui.openImportRPGMTransDialog();
						break;
						
					// exports
					case "exportToGamePatch":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportDir").trigger("click");
						break;
					case "exportToGamePatchZip":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#export").trigger("click");
						break;
					case "exportToCsv":
						$("#exportCSV").trigger("click");
						break;
					case "exportToXlsx":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportXLSX").trigger("click");
						break;
					case "exportToXls":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportXLS").trigger("click");
						break;
					case "exportToOds":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportODS").trigger("click");
						break;
					case "exportToHtml":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportHTML").trigger("click");
						break;
					case "exportToTransPatch":
						$("#dialogExport").data("options", {files:trans.getCheckedFiles()});
						$("#exportTrans").trigger("click");
						break;
					case "inject":
						ui.openInjectDialog();
						break;
					case "revert":
						trans.revertToOriginal();
						break;
					default :
					
					
				}
			}
			
			trans.updateRunScriptMenu()
			
			return {
				zIndex	:1000,
				callback: thisCallback,
				items	: trans.fileSelectorMenu
			}			
		}
	});	
	
	trans.fileSelectorContextMenuIsInitialized = true;
	
}

/**
 * Initialize grid's body context menu
 */
 Trans.prototype.gridBodyContextMenu = function() {
	$.contextMenu({
		selector: '.ht_master .htCore tbody, .ht_clone_left .htCore tbody', 
		events: {
			preShow : function($target, e) {
				//$(".context-menu-root").trigger("contextmenu:hide")
				var cTarget = $(e.target);
				console.log(cTarget);
				if (cTarget.hasClass("highlight")) {
					console.log("previously hightlighted");
				}
				console.log(arguments);
				
			},
			show : function($target, e){
				console.log("selection on show : ");
				trans.grid.lastContextMenuCellRange = trans.grid.getSelectedRange();
			},
			hide : function($target, e){
				console.log("reload selection : ");
				if (typeof trans.grid.lastContextMenuCellRange == "undefined") return false;
				trans.grid.selectCells(trans.grid.lastContextMenuCellRange);
				
				console.log(trans.grid.getSelectedRange());
			}
		},
		build: function($triggerElement, e) {
			var thisCallback = function(key, options) {
				switch (key) {
					case "addComment" :
						var thisCoord= undefined;
						try  {
							thisCoord = trans.grid.lastContextMenuCellRange[0]['highlight']
						} catch (error) {
							// do nothing
						}
						trans.editNoteAtCell(thisCoord);
						break;
					case "removeComment" :
					
						trans.removeNoteAtSelected(trans.grid.lastContextMenuCellRange);
						break;
					default :
					
					
				}
			}
			return {
				zIndex:1000,
				callback: thisCallback,
				items: {
					"addComment": {name: t("Add comment"), icon: function(){
						return 'context-menu-icon icon-commenting-o';
					}},					
					"removeComment": {name: t("Remove comment"), icon: function(){
						return 'context-menu-icon icon-comment-empty';
					}},					
					"selectAll" : {"name" : t("Select all")},
					"invertSelection" : {"name" : t("Invert selection")},
					"sep0": "---------",

					"withSelected": {
						name: "With all",
						items: {
							"batchTranslation": {"name" : t("Batch translation")},
							"wordWrap": {"name" : t("Wrap texts")}
						}
					}

				}
			}			
			
		}
	});	
	
}


//================================================================
//
// CONTEXT RELATED
//
//================================================================
/**
 * Sanitize query for contexts
 * @param {...String|...Array} - Context
 * @returns {String[]}
 */
 Trans.prototype.evalContextsQuery = function() {
	if (arguments.length == 0) return false;
	
	var result = [];
	for (var i=0; i<arguments.length; i++) {
		if (typeof arguments[i] == "string") {
			if (arguments[i].length == 0) continue;
			var thisA = arguments[i].split("\n").map(function(input) {
				return common.stripCarriageReturn(input);
			});
			result = result.concat(thisA);
		} else if (Array.isArray(arguments[i])) {
			if (arguments[i].length == 0) continue;
			result = result.concat(arguments[i]);
		}
	}
	
	return result;
}

/**
 * Check whether the a row has the context or not
 * @param {String} file - The file ID
 * @param {Number} row - The row to look for
 * @param {String[]} context - The context to check for
 * @returns {Boolean}
 */
 Trans.prototype.isInContext = function(file, row, context) {
	context = context||[];
	if (context.length == 0) return true;
	
	if (typeof context == 'string') context = [context];
	
	if (typeof trans.project.files[file] == 'undefined') return false;
	if (typeof trans.project.files[file].context[row] == 'undefined') return false;
	
	var thisContextS = trans.project.files[file].context[row];
	if (Array.isArray(thisContextS) == false) thisContextS = [thisContextS];

	if (thisContextS.length < 1) {
		// try to findout on parameters
		if (typeof trans.project.files[file].parameters[row] != 'undefined')  {
			thisContextS = [trans.buildContextFromParameter(trans.project.files[file].parameters[row])];
		}
		//continue;
	}	
	
	var contextStr = thisContextS.join("\n");

	for (var i=0; i<context.length; i++) {
		contextStr = contextStr.toLowerCase();
		if (contextStr.indexOf(context[i].toLowerCase()) != -1) return true;
	}

	return false;
}


/**
 * Deletes rows by context
 * @param {String} files 
 * @param {String[]} contexts 
 * @param {Object} options 
 * @param {Boolean} whitelist 
 */
 Trans.prototype.removeRowByContext = function(files, contexts, options, whitelist) {
	/*
		improved by. Vellithe
	*/
	options=options||{};
	options.matchAll = options.matchAll||false;
	
	var collection = trans.travelContext(files, contexts, {
		onMatch:function(file, row) {
			//trans.removeRow(file, row);
			//console.log("removing "+files+" row "+row);
		},
		matchAll:options.matchAll
	});
	
	for (var file in collection) {
		for (var row=collection[file].length-1; row>=0; row--) {
			if ((collection[file][row] == true && whitelist !== true) || (collection[file][row] != true && whitelist === true)) {
				console.log("removing "+file+" row "+row + (whitelist === true ? " (Not on whitelist)" : ""));
				trans.removeRow(file, row);
				//trans.project.files[file].data.splice(row, 1);
			}
		}
	}
	
	
	trans.refreshGrid();
}


/**
 * Generates context's keywords
 * @param {Object} [obj=trans.project] - Trans.project object
 * @param {String[]} [files] - List of the files 
 * @param {Object} [options] - Options
 * @returns {Object} Collection of the keywords
 */
 Trans.prototype.collectContextKeyword = function(obj, files, options) {
	files = files||[];
	obj = obj||trans.project;
	
	if (typeof obj == 'undefined') return false;
	
	if (typeof files == "string") files = [files];
	if (files.length < 1) { // select all
		for (let file in trans.project.files) {
			files.push(file);
		}
	}		
	
	//console.log(files);
	
	var collection = {};
	for (let i=0; i<files.length; i++) {
		let file = files[i];
		var thisData = obj.files[file].context;
		for (var contextId=0; contextId<thisData.length; contextId++) {
			if (Array.isArray(thisData[contextId]) == false) continue;
			for (var y=0; y<thisData[contextId].length; y++) {
				var contextString = thisData[contextId][y]||"";
				var contextPart = contextString.split("/");
				for (var x=0; x<contextPart.length; x++) {
					if (isNaN(contextPart[x])) {
						collection[contextPart[x]] = collection[contextPart[x]]||0;
						collection[contextPart[x]] += 1;
					}
				}
			}
		}
	}
	return collection;
}

/**
 * Iterate through contexts
 * @param {(String|String[])} files - Selected files
 * @param {(String|String[])} contexts - Context to search for
 * @param {Object} options 
 * @param {Function} options.onMatch 
 * @param {Function} options.onNotMatch 
 * @param {Boolean} options.matchAll 
 */
 Trans.prototype.travelContext = function(files, contexts, options) {
	//remove related context
	files 		= files||[];
	contexts 	= contexts||[]; // keywords
	options 			= options||{};
	options.onMatch 	= options.onMatch||function(){};
	options.onNotMatch 	= options.onNotMatch||function(){};
	options.matchAll 	= options.matchAll||false;
	
	
	if (typeof files == "string") files = [files];
	if (Array.isArray(contexts) == false) contexts = [contexts];
	

	if (files.length < 1) { // select all
		for (let file in trans.project.files) {
			files.push(file);
		}
	}
	//console.log(files);
	var collection = {};
	for (let i=0; i<files.length; i++) {
		let file = files[i];
		collection[file] = [];
		
		for (let rowId=0; rowId<trans.project.files[file].context.length; rowId++) {
			var thisContextS = trans.project.files[file].context[rowId];
			if (Array.isArray(thisContextS) == false) thisContextS = [thisContextS];
			collection[file][rowId] = false;
			if (thisContextS.length < 1) {
				// try to findout on parameters
				if (!trans.project.files[file].parameters[rowId]) continue;
				thisContextS = [trans.buildContextFromParameter(trans.project.files[file].parameters[rowId])];
				//continue;
			}
			

			for (var y=0; y<thisContextS.length; y++) {
				var thisContext = thisContextS[y];
				//console.log(thisContext);
				for (var x=0; x<contexts.length; x++) {
					//try {
					if (options.matchAll) {
						if (common.matchAllWords(thisContext, contexts[x])) {
							collection[file][rowId] = true;
						}
					} else {
						//console.log("comparing "+thisContext+" with "+contexts[x]);
						if (thisContext.toLowerCase().indexOf(contexts[x].toLowerCase()) >= 0) {
							//console.log("match");
							//matchFound = true;
							
							//if (options.onMatch.call(trans.project.files[file], file, rowId) === false) return false;
							collection[file][rowId] = true;
							//break;
						} else {
							//if (options.onNotMatch.call(trans.project.files[file], file, rowId) === false) return false;
						}
					}
					//} catch(err) {
						
					//}
				}
			}
		}
		
		for (let rowId=0; rowId<collection[file].length; rowId++) {
			if (collection[file][rowId] == true) {
				options.onMatch.call(trans.project.files[file], file, rowId);
			} else {
				options.onNotMatch.call(trans.project.files[file], file, rowId);
			}
		}
	}
	
	return collection;
}


//================================================================
//
// UTILITY
//
//================================================================

Trans.prototype.isOrganicCell = function(row, col, file) {
	if (!this.project) return false;
	file ||= this.getSelectedId();
	var cellInfo = this.cellInfo.getCell(file, row, col);
	if (cellInfo?.t == "HU") return true;
	return false;
}

Trans.prototype.isVisitedCell = function(row, col, file) {
	if (!this.project) return false;
	file ||= this.getSelectedId();
	return Boolean(this.cellInfo.get("v", file, row, col));
}

/**
 * Get data from the file object
 * @param  {(Object|String|undefined)} file - File ID or File Object or undefined 
 * @returns {String[][]} The array representation of the table
 */
 Trans.prototype.getData = function(file) {
	if (typeof file == 'undefined') return this.getCurrentData();
	if (typeof file == "string") return this.getObjectById(file).data;
	if (typeof file == 'object') {
		if (Array.isArray(file.data)) return file.data;
	}

	console.warn("invalid id or object ", file);
	return [];
}

/**
 * Add a new key into a data
 * @param  {Any} file - File ID or File Object or undefined 
 * @param  {String} keyString - keyString
 * @param  {String} defaultTranslation - Default translation
 * @returns {Integer} the index of the new inserted data
 */
 Trans.prototype.addRow = function(file, keyString, defaultTranslation) {
	var thisObj
	if (typeof file == "object") {
		thisObj = file;
	} else {
		file = file || this.getSelectedId();
		thisObj = this.getObjectById(file);
	}
	thisObj.indexIds ||= {};

	if (typeof keyString !== "string") return -1;
	if (!keyString) return -1;
	const existingIndex = this.getIndexByKey(thisObj, keyString);
	console.log("Existing index is", existingIndex);
	if (typeof existingIndex !== "undefined") return existingIndex;
	var newRow = Array(trans.colHeaders.length).fill(null);
	newRow[this.keyColumn] = keyString;
	if (defaultTranslation) newRow[1] = defaultTranslation;

	console.log("inserting row:", newRow);
	var theData = trans.getData(thisObj);
	console.log("theData", theData)
	if (empty(theData[theData.length - 1][0])) {
		// overwrite the last empty cell
		let thisIndex = theData.length - 1
		theData[thisIndex] = newRow;
		thisObj.indexIds[keyString] = thisIndex;
		return thisIndex;
	} else {
		var newKey = theData.push(newRow);
		thisObj.indexIds[keyString] = newKey -1;
		return newKey;
	}

}

/**
 * Get indexes of a fileId
 * @param {String|Object} [file=this.getSelectedId()] - File id or file object to be indexed
 * @returns {Object} Key-Value pair of the key text and its row index
 */
 Trans.prototype.getIndexIds = function(file) {
	if (typeof file == 'undefined') file = this.getSelectedId();
	var theObject;
	if (typeof file == "string") {
		theObject = this.getObjectById(file);
		if (!theObject) return {}
		if (!theObject.indexIsBuilt) this.buildIndex(file);
		return theObject.indexIds;
	} else if (typeof file == 'object') {
		theObject = file;
		if (!theObject.indexIsBuilt) theObject = this.buildIndexFromData(theObject);
		return theObject.indexIds;
	}

	console.warn("Error getting Index ID for file:", file);
	return {};
}

/**
 * Get row index based on the key string
 * @param {String|Object} file - File ID or File Object to be searched
 * @param {String} keyString - Key string to look for
 * @returns {Number|undefined} - Row id of the keyString if exist. Undefined if not exist.
 */
 Trans.prototype.getIndexByKey = function(file, keyString) {
	return this.getIndexIds(file)[keyString];
}

/**
 * Check whether all available files are checked
 * @returns {Boolean} True if all files are checked
 */
 Trans.prototype.isAllSelected = function() {
	if (trans.getCheckedFiles().length == Object.keys(trans.project.files).length) return true;
	return false;
}

/**
 * Get the current active translator engine's ID
 * @returns {String} Active translator ID
 */
 Trans.prototype.getActiveTranslator = function() {
	if (!trans.project) return sys.config?.translator;
	trans.project.options = trans?.project.options || {};
	return trans.project?.options?.translator || sys.config.translator;
}

/**
 * Append text to the common reference
 * @param {String} text - Key text to append
 * @returns {Boolean} Return false if failed
 */
Trans.prototype.appendTextToReference = function(text) {
	if (typeof text !== 'string') return false;
	if (Boolean(text)==false) return false;
	
	if (trans.isKeyExistOn(text, "Common Reference")) return trans.alert("Unable to add <b>"+text+"</b>. That value already exist on Common Reference!");
	
	
	var ref= trans.project.files["Common Reference"];
	var lastKey = ref.data.length-1;
	if (Boolean(ref.data[lastKey][0]) == false) {
		console.log("inserting to ref.data[lastKey][0]");
		ref.data[lastKey][0] = text;
		ref.indexIds[text] = lastKey;
	} else {
		console.log("append new data");
		var newData = new Array(trans.colHeaders.length);
		newData = newData.fill(null);
		newData[0] = text;
		ref.data.push(newData);
		ref.indexIds[text] = ref.data.length-1;
	}
	
	trans.alert("<b>"+text+"</b> "+t("added to reference table!"));	
	return true;
}

/**
 * Word wrap a file object
 * @param {String[]} [files=this.getAllFiles()] - List of files to be processed
 * @param {Number} [col=1]  - Column ID to be processed
 * @param {Number} [targetCol=col+1] - Column of the processed text will put into
 * @param {Object} [options]
 * @param {Number} [options.maxLength=41] - Maximum length of the line
 * @param {String[]} [options.context] - Context filter. Only the rows that has this context will be processed
 * @param {Function} [options.onDone] - Triggered when the process is completed
 */
 Trans.prototype.wordWrapFiles = function(files, col, targetCol, options) {

	files = files||[];
	if (typeof files == 'string') files = [files];
	
	if (files.length == 0 ) files = this.getAllFiles();
	//console.log(arguments);
	//return true;
	
	col = col||1;
	targetCol = targetCol||col+1;
	if (targetCol == 0) return trans.alert(t("Can not modify Column 0"));
	
	options = options||{};
	options.maxLength 	= options.maxLength||41; // default with picture, without picture is 50
	options.onDone 		= options.onDone||function() {};
	options.context 	= options.context||[] // context filter
	
	for (var id=0; id<files.length; id++) {
		var file = files[id];
		console.log("Wordwrapping file : "+file);
		if (typeof trans.project.files[file] == 'undefined') continue;
		options.lineBreak = options.lineBreak||trans.project.files[file].lineBreak||"\n";
		
		var thisData = trans.project.files[file].data;
		//console.log(thisData);
		for (var row=0; row<thisData.length; row++) {
			if (!trans.isInContext(file, row, options.context)) continue;
			if (typeof thisData[row][col] !== 'string') {
				thisData[row][targetCol] = thisData[row][col];
			}
			
			thisData[row][targetCol] = common.wordwrapLocale(thisData[row][col], options.maxLength, this.getTl(), options.lineBreak);
		}
	}
	
	options.onDone.call(trans);
	
}

/**
 * Fill empty lines from other column
 * @param {String|String[]} files - File ID or list of file ID
 * @param {Number[]} rows - Row ID or list of row ID
 * @param {Number} targetCol - Target column
 * @param {Number} sourceCol 
 * @param {Object} options 
 * @param {Object} [options.project=trans.project] 
 * @param {Object} [options.keyColumn=0] 
 * @param {Function} [options.lineFilter] 
 * @param {Boolean} [options.fromKeyOnly=false] 
 * @param {String[]} [options.filterTag] 
 * @param {Boolean} [options.overwrite=false] 
 */
 Trans.prototype.fillEmptyLine = function(files, rows, targetCol, sourceCol, options) {
	/*
	Integer targetCol
	Integer sourceCol
	*/
	// if targetCol is undefined, than the right most row with existed translation will be picked
	files = files||[];
	if (typeof files == 'string') files = [files];
	options = options||{};
	options.project 	= options.project||trans.project;
	options.keyColumn 	= options.keyColumn||0;
	options.lineFilter 	= options.lineFilter|| function() {return true};
	options.fromKeyOnly	= options.fromKeyOnly || false; // fill from key column only
	options.filterTag 	= options.filterTag || [];
	options.overwrite	= options.overwrite || false;
	
	if (options.fromKeyOnly) {
		console.warn("collecting data from key only");
		options.sourceCol  = sourceCol||options.keyColumn;
	}
	
	rows = rows||[];
	if (typeof rows == 'number') rows = [rows];
	
	if (files.length == 0) files = trans.getAllFiles();
	console.log(files);
	
	for (var index=0; index<files.length; index++) {
		let file = files[index];
		//console.log(file);
		//var thisLineBreak = options.project.files[file].thisLineBreak||"\n";
		if (rows.length > 0) {
			// do nothing
		} else { // all row
			var thisData = options.project.files[file].data;
			for (var row=0; row<thisData.length; row++) {
				
				if (options.filterTagMode == "blacklist") {
					if (this.hasTags(options.filterTag, row, file)) continue;
				} else if (options.filterTagMode == "whitelist") {
					if (!this.hasTags(options.filterTag, row, file)) continue;
				}
				
				if (typeof targetCol == 'undefined') {
					targetCol = trans.getTranslationColFromRow(thisData[row]);
					if (targetCol == null) continue; // no translation exist
				}
				
				if (options.overwrite == false) {
					if (thisData[row][targetCol]) continue;
				}

				/*
				if (typeof sourceCol == 'undefined') {
					sourceCol = trans.getTranslationColFromRow(thisData[row], targetCol); // get translation except targetCol
					if (sourceCol == null) continue; // no source found
				}
				*/
				options.project.files[file].data[row][targetCol] = trans.getTranslationByLine(thisData[row], options.keyColumn, {
					includeIndex	:true,
					priorityCol		:targetCol,
					onBeforeLineAdd	:options.lineFilter,
					sourceCol		:options.sourceCol//column to check, undefined means all
				});
				
			}			
		}
	}
	
}

/**
 * remove whitespace from translation
 * @param {(String|String[])} files - File id(s) to process
 * @param {(Number|Number[])} columns - column to process
 * @param {Object} options 
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.trimTranslation = function(files, columns, options) {
	// remove whitespace from translation
	if (!trans.project) return false;
	files = files||trans.getSelectedId();
	options = options||{};
	options.refreshGrid = options.refreshGrid||false;
	
	if (Array.isArray(files) == false) files = [files];
	if (Array.isArray(columns) == false) columns = [columns];
	
	
	for (var i=0; i<files.length; i++) {
		var file = files[i];
		//console.log("handling "+file);
		var thisData = trans.project.files[file].data;
		//var thisLineBreak = trans.project.files[file].lineBreak||"\n";
		var originalLineBreak = trans.project.files[file].lineBreak||"\n";
		var thisLineBreak = "\n";
		for (var row=0; row<thisData.length; row++) {
			//console.log("handling row "+row);
			for (var colID in columns) {
				var col = columns[colID];
				//console.log("handling col "+col);
				//console.log(trans.project.files[file].data[row][col]);
				if (col < 1) continue;
				if (typeof trans.project.files[file].data[row][col] !== 'string') continue;
				var lines = trans.project.files[file].data[row][col].split(thisLineBreak);
				var newLines = lines.map(function(thisVal) {
					//console.log(thisVal.trim());
					return thisVal.trim();
				});
				trans.project.files[file].data[row][col] = newLines.join(originalLineBreak);
			}
		}
	}
	
	//if (options.refreshGrid) {
		trans.refreshGrid();
	//}	
}

/**
 * Copy left padding of the key texts into translations
 * @param {(String|String[])} files - File id(s) to process
 * @param {(Number|Number[])} columns - column to process
 * @param {Object} options 
 * @param {Number} [options.keyId=0] - The index of the key column
 * @param {Boolean} options.includeInitialWhitespace 
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.paddingTranslation = function(files, columns, options) {
	// Copy left padding from keys to translations
	if (!trans.project) return false;
	files = files||trans.getSelectedId();
	options								= options||{};
	options.keyId 						= options.keyId||0;
	options.includeInitialWhitespace 	= options.includeInitialWhitespace||false;
	options.refreshGrid 				= options.refreshGrid||false;
	
	if (Array.isArray(files) == false) files = [files];
	if (Array.isArray(columns) == false) columns = [columns];
	
	//var whiteSpaces = /^[ \s\u00A0\f\n\r\t\v\u00A0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u2028\u2029\u202f\u205f\u3000]+/g
	//var whiteSpaces = /^[\r\n\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\u0009\u200b\u180e\u2060]+/g;
	var whiteSpaces = /^[\s\u0009\u200b\u180e\u2060]+/g

	for (var i=0; i<files.length; i++) {
		var file = files[i];
		var thisData = trans.project.files[file].data;
		var thisLineBreak = trans.project.files[file].lineBreak||"\n";
		for (var row=0; row<thisData.length; row++) {
			if (typeof trans.project.files[file].data[row][options.keyId] !== 'string') continue;
			var keys = trans.project.files[file].data[row][options.keyId].split(thisLineBreak);
			var leftWhiteSpaces = [];
			for (var keysID=0; keysID<keys.length; keysID++) {
				var thisLeftWS = keys[keysID].match(whiteSpaces);
				if (Boolean(thisLeftWS) == false) thisLeftWS = "";
				leftWhiteSpaces.push(thisLeftWS);
			}
			
			for (var colID in columns) {
				var col = columns[colID];
				if (col < 1) continue;
				if (typeof trans.project.files[file].data[row][col] !== 'string') continue;
				
				var lines = trans.project.files[file].data[row][col].split(thisLineBreak);
				var newLines = [];
				
				for (var linePartId=0; linePartId<lines.length; linePartId++) {
					var thisWhitespace = leftWhiteSpaces[linePartId]||"";
					if (options.includeInitialWhitespace) {
						newLines.push(thisWhitespace+lines[linePartId]);
					} else {
						newLines.push(thisWhitespace+lines[linePartId].trim());
					}
				}
				
				trans.project.files[file].data[row][col] = newLines.join(thisLineBreak);
			}
		}
	}
	
	//if (options.refreshGrid) {
		trans.refreshGrid();
	//}	
}

/**
 * Clear translations from selected files
 * @param {(String|String[])} files - File id(s)
 * @param {Object} options 
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.removeAllTranslation = function(files, options) {
	if (!trans.project) return false;
	files = files||trans.getSelectedId();
	options = options||{};
	options.refreshGrid = options.refreshGrid||false;
	
	if (Array.isArray(files) == false) files = [files];
	
	for (var i=0; i<files.length; i++) {
		var file = files[i];
		var thisData = trans.project.files[file].data;
		for (var row=0; row<thisData.length; row++) {
			for (var col=1; col<thisData[row].length; col++) {
				trans.project.files[file].data[row][col] = null;
			}
		}
	}
	
	/**
	 * Triggered when all all translation are removed
	 * @event Trans#removeAllTranslation
	 * @param  {Object} Options
	 * @param  {String[]} Options.files - List of the file ids
	 * @param  {Object} Options.options - Options
	 */
	this.trigger("removeAllTranslation", {files:files, options:options});
	if (options.refreshGrid) {
		trans.refreshGrid();
	}
}

/**
 * Delete files
 * @param {(String|String[])} files - Files to be deleted
 * @param {Object} options 
 * @returns {Boolean} True on success
 */
 Trans.prototype.deleteFile = function(files, options) {
	if (!trans.project) return false;
	files = files||trans.getSelectedId();
	options = options||{};
	
	if (Array.isArray(files) == false) files = [files];
	
	if (files.length < 1) return true;
	
	for (var i=0; i<files.length; i++) {
		// unselect if selected
		var file = files[i];
		
		if (trans.project.files[file].type == 'reference') {
			alert(t("Unable to delete table : ")+file);
			continue;
		}
		
		
		if (file == trans.getSelectedId()) {
			ui.disableGrid(true);
		}
		
		
		var bak = JSON.parse(JSON.stringify(trans.project.files[file]));
		trans.project.trash = trans.project.trash||{};
		trans.project.trash[file] = bak;
		
		$(".panel-left .fileList [data-id="+common.escapeSelector(file)+"]").remove();
		delete trans.project.files[file];
	}
	ui.fileList.reIndex();

	this.trigger("afterDeleteFile", [files]);
}

/**
 * Remove one or more files from staging directory
 * This function is useful for cleaning up staging files for example after removing the project's object
 * @async
 * @param {String[]} files - List of files to be removed
 * @since 4.4.4
 */
Trans.prototype.stagingFilesRemove = async function(files) {
	if (!Array.isArray(files)) files = [files];
	var stagingPath = this.getStagingPath();
	if (!stagingPath) return [];
	var result = []
	for (var i in files) {
		var fileToRemove = nwPath.join(stagingPath, "data", files[i]);
		console.log("Removing", fileToRemove);
		if (!await common.isFileAsync(fileToRemove)) console.log("Not found:", fileToRemove);
		result.push(await common.unlink(files[i]));
	}
	return result;
}

/**
 * Removes row
 * @param {String} file - File to be processed
 * @param {(Number|Number[])} rows - Rows to be removed
 * @param {Object} options 
 * @param {Boolean} options.permanent - will put into the temporary bucket when false
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.removeRow = function(file, rows, options) {
	console.log("removing row : ", arguments);
	if (typeof file == 'undefined') return false;
	if (typeof rows == 'undefined') return false;
	if (rows === 0) rows = [0];
	rows = rows||[];
	options = options||{};
	if (Array.isArray(rows) == false) rows = [rows];
	options.permanent 	= options.permanent||false;
	options.refreshGrid = options.refreshGrid||false;
	
	// make the array unique, so one row can be only removed once
	rows = common.arrayUnique(rows);

	// sort array descending! this is important!
	rows.sort(function(a, b) {
		return b - a;
	});
	console.log("Removing rows > should be ordered descendingly:", rows);
	
	for (var i=0; i<rows.length; i++) {
		var thisRow = rows[i];
		if (typeof trans.project.files[file].data[thisRow] == 'undefined') continue;
		trans.project.files[file].data.splice(thisRow, 1);
		if (trans.project.files[file].parameters) trans.project.files[file].parameters.splice(thisRow, 1);
		if (trans.project.files[file].context) trans.project.files[file].context.splice(thisRow, 1);
		if (trans.project.files[file].tags) trans.project.files[file].tags.splice(thisRow, 1);
	
		// adjust cellInfo
		this.cellInfo.deleteRow(file, thisRow);

		// adjust comment
		var comments = this.getObjectById(file).comments;
		if (empty(comments)) continue;
		if (Array.isArray(comments)) {
			comments.splice(thisRow, 1);
		} else {
			delete comments[thisRow];
		}

	}
	
	if (rows.length > 0) trans.project.files[file].indexIsBuilt = false;
	
	
	/**
	 * Triggered after removing rows
	 * @event Trans#afterRemoveRow
	 * @param  {Object} options
	 * @param  {String} options.file - file id
	 * @param  {Number[]} options.rows - List of rows
	 * @param  {Object} options.options
	 */
	this.trigger("afterRemoveRow", {file:file, rows:rows, options:options});

	if (options.refreshGrid) {
		trans.refreshGrid();
	}
}

/**
 * Clear translations from rows
 * @since 4.11.30
 * @param {Object|String} file - File ID or file object
 * @param {Number|Number[]} rows - A row or array of rows.
 * @param {Object} [options] - Options
 * @returns {Number} - Affected row[s]
 */
Trans.prototype.clearRow = function(file, rows, options={}) {
	if (typeof file == "string") file = trans.getObjectById(file);
	if (!file) return;
	if (!(typeof file == "object" && Array.isArray(file.data))) return console.warn("Invalid argument 1");

	if (!Array.isArray(rows)) rows = [rows];
	options ||= {};
    var affectedRows = 0;
	for (var rowId=0; rowId<rows.length; rowId++) {
		var row = file.data[rows[rowId]];
		for (var colId=0; colId<row.length; colId++) {
			if (colId == this.keyColumn) continue;
			row[colId] = "";
            affectedRows++;
		}
	}
    return affectedRows;
}

/**
 * Removes a column
 * This will affect the entire files
 * @param {Number} column - Column to remove
 * @param {Object} options 
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.removeColumn = function(column, options) {
	if (column === 0) return trans.alert(t("Can not remove key column!"));
	options = options||{};
	options.permanent = options.permanent||false;
	options.refreshGrid = options.refreshGrid||false;
	if(typeof trans.project == "undefined") return trans.alert(t("Please open or create a project first"));
	
	for (var file in trans.project.files) {
		if(Array.isArray(trans.project.files[file].data) == false) continue;
		if(trans.project.files[file].data.length == 0) continue;
		for (var row=0; row< trans.project.files[file].data.length; row++) {
			trans.project.files[file].data[row].splice(column, 1);
			
			// adjust cellInfo
			this.cellInfo.deleteCell(file, row, column);
		}
	}
	
	trans.colHeaders.splice(column, 1);
	trans.columns.splice(column, 1);
	
	if (options.refreshGrid) {
		trans.refreshGrid();
	}
}

/**
 * Rename a column
 * @param {Number} column 
 * @param {String} newName 
 * @param {Object} options 
 * @param {Boolean} options.refreshGrid - Refresh the current grid after process is completed
 */
 Trans.prototype.renameColumn = function(column, newName, options) {
	if (column === 0) return trans.alert(t("Can not set column name to blank!"));
	options = options||{};
	options.refreshGrid = options.refreshGrid||false;
	
	if (typeof trans.colHeaders[column] == 'undefined') return false; 
	trans.colHeaders[column] = newName;
	
	if (options.refreshGrid) {
		trans.refreshGrid();
	}
	
}

/**
 * Check whether a row has multiple context
 * @param {Number} row - Row to check
 * @param {Object} [obj=trans.getSelectedObject()] - Active object
 * @returns {Boolean} True if the row has more than one context
 * @since 4.10.18
 */
Trans.prototype.rowHasMultipleContext = function(row, obj) {
	obj = obj || this.getSelectedObject();
	if (!row) false;
	if (!obj.context) return false;
	if (!obj.context[row]) return false;
	if (obj.context[row].length <= 1) return false;
	return true;
}

/**
 * Check whether the row is translated or not
 * @param {Number} row - Index of the row
 * @param {String[][]} data - Two dimensional array represents the table
 * @returns {Boolean} True if the row has translation
 */
 Trans.prototype.isTranslatedRow = function(row, data) {
	data = data||trans.data;
	for (var col=1; col < data[row].length; col++) {
		var thisCell = data[row][col]||"";
		if (thisCell.length > 0) return true;
	}
	return false;
}

/**
 * Count how many cells are translated on the selected row
 * Row index are not counted
 * @param {Number} row - Row index 
 * @param {String[][]} data - Two dimensional array represents the table
 * @returns {Number} The number of the translated cells
 */
 Trans.prototype.countFilledCol = function(row, data) {
	// exclude col index 0
	data = data||trans.data;
	var result = 0;
	for (var col=1; col < data[row].length; col++) {
		var thisCell = data[row][col]||"";
		if (thisCell.length > 0) result++;
	}
	return result;
}

/**
 * Retrieve the best translation in an array with Translator++'s rule
 * The rightmost cell got the priority
 * @param {String[]} row - Array of text
 * @param {Number} [keyColumn=0] - Index of the key column
 * @returns {String}
 */
 Trans.prototype.getTranslationFromRow = function(row, keyColumn, skipRows=[]) {
	// retrieve best translation in an array
	//console.log("Get translation from row", arguments);
	skipRows ||= []
	if (Array.isArray(row) == false) return false;
	keyColumn = keyColumn||0;
	if (keyColumn == 0) {
		for (let n=row.length; n>0; n--) {
			if (skipRows.includes(n)) continue;
			if (row[n]) {
				return row[n];
			}
		}
	} else {
		for (let n=row.length; n>=0; n--) {
			if (n == keyColumn) continue;
			if (skipRows.includes(n)) continue;
			if (row[n]) {
				return row[n];
			}
		}
	}
	return null;
}

/**
 * Retrieve the best translation in an array with Translator++'s rule
 * The rightmost cell got the priority
 * @param {String[]} row - Array of text
 * @param {Number} [keyColumn=0] - Index of the key column
 * @returns {Number} Cell index of the translation
 */
 Trans.prototype.getTranslationColFromRow = function(row, keyColumn) {
	if (Array.isArray(row) == false) return false;
	keyColumn = keyColumn||0;
	if (keyColumn == 0) {
		for (let n=row.length; n>keyColumn; n--) {
			if (row[n]) {
				return n;
			}
		}
	} else {
		for (let n=row.length; n>=0; n--) {
			if (n == keyColumn) continue;
			if (row[n]) {
				return n;
			}
		}
	}
	return null;
}

/**
 * Retrieve the best translation in an array with Translator++'s rule
 * The rightmost cell got the priority
 * Used in line-by-line translation
 * @param {String[]} row - Array of text
 * @param {Number} [keyColumn=0] - Index of the key column
 * @param {Object} [options]
 * @param {Object} [options.includeIndex]
 * @param {String} [options.lineBreak=\n] - Line break character
 * @param {Function} [options.onBeforeLineAdd]
 * @returns {String} The best translation
 */
 Trans.prototype.getTranslationByLine = function(row, keyColumn, options) {
	// get line by line best translation
	if (Array.isArray(row) == false) return false;
	//console.log(arguments);
	keyColumn 				= 0;
	options 				= options||{};
	options.includeIndex 	= options.includeIndex||false;
	options.lineBreak 		= options.lineBreak||"\n";
	options.onBeforeLineAdd = options.onBeforeLineAdd||function() {return true};
	//options.priorityCol 	= options.priorityCol||undefined;
	//options.sourceCol 	= options.sourceCol||undefined;
		
	var resultArray = [];
	
	if (typeof options.sourceCol != 'undefined') {
		let thisCell = row[options.sourceCol]||"";
		
		let thisCellPart = thisCell.split("\n").map(function(input){
								return common.stripCarriageReturn(input);
							});
		for (let part=0; part<thisCellPart.length; part++) {
			if (Boolean(thisCellPart[part]) == false) continue;
			if (!options.onBeforeLineAdd(thisCellPart[part])) continue;
			resultArray[part] = thisCellPart[part];
		}			
	} else {
		for (let col=0; col<row.length; col++) {
			if (col == keyColumn) continue;
			
			if (typeof options.priorityCol !=='undefined') {
				if (col == options.priorityCol) continue;
			}
			
			let thisCell = row[col]||"";
			let thisCellPart = thisCell.split("\n").map(function(input){
									return common.stripCarriageReturn(input);
								});
			for (let part=0; part<thisCellPart.length; part++) {
				if (Boolean(thisCellPart[part]) == false) continue;
				if (!options.onBeforeLineAdd(thisCellPart[part])) continue;
				
				resultArray[part] = thisCellPart[part];
			}
		}
	}
	
	if (options.includeIndex) {
		let thisCell = row[keyColumn]||"";
		
		let thisCellPart = thisCell.split("\n").map(function(input){
								return common.stripCarriageReturn(input);
							});
		for (let part=0; part<thisCellPart.length; part++) {
			if (Boolean(thisCellPart[part]) == false) continue;
			if (!options.onBeforeLineAdd(thisCellPart[part])) continue;
			
			resultArray[part] = thisCellPart[part];
		}		
		
	}
	
	if (typeof options.priorityCol !== 'undefined') {

		var thisCell = row[options.priorityCol]||"";
		
		var thisCellPart = thisCell.split("\n").map(function(input){
								return common.stripCarriageReturn(input);
							});
		for (var part=0; part<thisCellPart.length; part++) {
			if (Boolean(thisCellPart[part]) == false) continue;
			//if (!options.onBeforeLineAdd(thisCellPart[part])) continue;
			
			resultArray[part] = thisCellPart[part];
		}			
	}	
	
	return resultArray.join(options.lineBreak);
	
	
}

/**
 * Generates translation pair
 * @param {String[][]} data - Two dimensional array represents the table
 * @param {Number} translationCol - Column index of the preferred translation
 * @returns {Object} - Key-value pair of translation
 */
 Trans.prototype.generateTranslationPair = function(data, translationCol) {
	translationCol = translationCol || 0;
	if (Array.isArray(data) == false) return {};
	var result = {};
	for (var rowId=0; rowId<data.length; rowId++) {
		if (Boolean(data[rowId][0]) == false) continue;
		var translation = trans.getTranslationFromRow(data[rowId], translationCol);
		if (translation == null) continue;
		result[data[rowId][0]] = translation;
	}
	return result;
	
}

/**
 * Count how many rows are translated
 * @param {(String|String[])} file - The file ID(s) to process
 * @returns {TranslationStats}
 */
 Trans.prototype.countTranslated = function(file) {
	file = file||[];
	if (typeof file == 'string') {
		file=[file];
	}
	
	if (file.length == 0) file = this.getAllFiles()
	
	var result = {};
	for (var i=0; i<file.length; i++) {
		var thisData = this.project.files[file[i]].data;
		/**
		 * @typedef {Object} TranslationStats
		 * @property {number} translated - How many rows are translated
		 * @property {number} length - The number rows in total
		 * @property {number} percent - How many rows are translated
		 */
		result[file[i]] = {
			translated	:0,
			length		:0,
			percent		:100
		};
		
		for (var row=0; row<thisData.length; row++) {
			if (Boolean(thisData[row][this.keyColumn]) == false) continue;
			thisData[row][this.keyColumn] = thisData[row][this.keyColumn]||"";
			// stringify 
			thisData[row][this.keyColumn] = thisData[row][this.keyColumn]+""

			if (this.rowByRowMode) {
				// DO NOT USE, evaluating by rows
				try {
					var thisKeyCount = thisData[row][0].split("\n").length;
				} catch (e) {
					console.error("Error when trying to split key string ", thisData[row][0]);
					throw(e)
				}
	
				for (var col=1; col<thisData[row].length; col++) {
					if (Boolean(thisData[row][col]) == false) continue;
					// converts non string cell into string
					if (typeof(thisData[row][col]) !== 'string') thisData[row][col] = thisData[row][col]+'';
					thisData[row][col] = thisData[row][col]||"";
					var thisColCount = thisData[row][col].split("\n").length;
					
					if (thisColCount>=thisKeyCount) {
						result[file[i]].translated ++;
						break;
					}
				}
			} else {
				if (this.rowHasTranslation(thisData[row], this.keyColumn)) result[file[i]].translated++;
			}
			
			result[file[i]].length++;

		}
		if (result[file[i]].length > 0) result[file[i]].percent = result[file[i]].translated/result[file[i]].length*100;
		if (result[file[i]].percent > 100) result[file[i]].percent = 100;
		if (result[file[i]].percent < 0) result[file[i]].percent = 0;
		this.project.files[file[i]].progress = result[file[i]];
	}
	
	return result;
}

/**
 * Resets the index of all files
 */
 Trans.prototype.resetIndex = function(hardReset) {
	hardReset = hardReset || false;
	for (var id in this.project.files) {
		this.project.files[id].indexIsBuilt = false;
	}
}

/**
 * building indexes from trans data for faster search KEY by ID
 * This function will cache the result in default index's cache location which is: `trans.project.files[fileId].indexID` 
 * @param {String} fileId - The file ID to process
 * @param {Boolean} [rebuild=false] - Force to rebuld index if index is already exist
 * @param {Number} [keyColumn=0] - Key column of the table
 * @returns {Object} Key-value pair of the index
 */
 Trans.prototype.buildIndex = function(fileId, rebuild, keyColumn) {

	fileId = fileId||trans.getSelectedId();
	keyColumn = keyColumn || trans.keyColumn || 0;
	
	if (fileId) {
		var currentObject = trans.project.files[fileId];
		if (currentObject.indexIsBuilt && rebuild !== true) return currentObject.indexIds;
		
		var result = {};
		for (let i=0; i<currentObject.data.length; i++) {
			//console.log("registering : "+currentObject.data[i][0]);
			if (typeof currentObject.data[i] == 'undefined') continue;
			if (currentObject.data[i][keyColumn] == null || currentObject.data[i][keyColumn] == '' || typeof currentObject.data[i][keyColumn] == 'undefined') continue;
			result[currentObject.data[i][keyColumn]] = i;
		}
		currentObject.indexIds 		= result;
		currentObject.indexIsBuilt 	= true;
		
		if (fileId == trans.getSelectedId()) {
			trans.indexIds 		= currentObject.indexIds;
			trans.indexIsBuilt 	= currentObject.indexIsBuilt;
		}
		return result;
	} else {
		if (trans.indexIsBuilt && rebuild !== true) return trans.indexIds;
	
		for (let i=0; i<trans.data.length; i++) {
			if (typeof trans.data[i] == 'undefined') continue;
			if (trans.data[i][keyColumn] == null || trans.data[i][keyColumn] == '' || typeof trans.data[i][keyColumn] == 'undefined') continue;
			trans.indexIds[trans.data[i][keyColumn]] = i;
		}
		trans.indexIsBuilt = true;
		return trans.indexIds;
	}
}

/**
 * Build indexes from multiple files
 * @param {String[]} files - The file ID to process
 * @param {Boolean} lineByLine - Whether the index processed with line-by-line algorithm or no. Default row-by-row algorithm
 * @param {Object} options
 * @returns {Object} Key-Object of the value pair of the index
 * @since 4.7.15
 */
Trans.prototype.buildIndexes = function(files, lineByLine=false, options={}) {
	options ||= {};
	options.customFilter;
	options.indexId ||= "";

	//todo : custom index
	this.customIndexes ||= {};
	if (typeof options.customFilter == "function") {
		console.log("Generating custom index mode");
		try {
			if (typeof options.customFilter("test") !== "string") return console.error("Invalid customFilter. Custom filter should return string")
		} catch (e) {
			return console.error("Invalid customFilter. Custom filter should return string")
		}
		options.indexId = "#auto.fn."+common.crc32String(options.customFilter.toString());
	}

	if (options.indexId) {
		this.customIndexes[options.indexId] = {}
	}

	if (typeof files == "string") files = [files];
	files = files || [];
	if (files.length == 0) { // that means ALL!
		files = this.getAllFiles();
	}

	var result = {};

	if (lineByLine) {
		for (let i=0; i<files.length; i++) {
			let thisObj = this.getObjectById(files[i]);
			if (!thisObj) continue;
			if (!thisObj.indexIsBuilt) this.buildIndex(files[i]);
			let thisIndexIds = this.getObjectById(files[i]).indexIds;
			for (var keyText in thisIndexIds) {
				var keys = keyText.replaceAll("\r", "").split("\n");
				for (var line=0; line<keys.length;line++) {
					let key = keys[line];
					if (typeof options.customFilter == "function") key = options.customFilter(key);
					result[key] = result[key] || [];
					result[key].push({
						file	:files[i],
						row		:thisIndexIds[keyText],
						line	:line
					})
				}

			}
		}

		if (options.indexId) {
			this.customIndexes[options.indexId] = result;
		} else {
			this._tempIndexes = result;
		}

		return result;
	}


	// row by row algorithm
	console.log("Files is", files);
	for (let i=0; i<files.length; i++) {
		console.log("handling file:", files[i]);
		let thisObj = this.getObjectById(files[i]);
		console.log("ThisObject:", thisObj);
		if (!thisObj) continue;
		if (!thisObj.indexIsBuilt) this.buildIndex(files[i]);
		let thisIndexIds = this.getObjectById(files[i]).indexIds;
		for (let key in thisIndexIds) {
			if (typeof options.customFilter == "function") key = options.customFilter(key);
			result[key] = result[key] || [];
			result[key].push({
				file:files[i],
				row:thisIndexIds[key]
			})
		}
	}

	if (options.indexId) {
		this.customIndexes[options.indexId] = result;
	} else {
		this._tempIndexes = result;
	}
	console.log("Indexes is", result);
	return result;
}

/**
 * Get data from index
 * @param {String} keyword - Keyword to search for
 * @param {Object} [indexes] - Key value pair of indexes
 * @param {Function} [customFilter] - Function custom filter used when building the index
 * @returns {String|undefined}
 */
Trans.prototype.getFromIndexes = function(keyword, indexes, customFilter) {
	if (typeof customFilter == "function") {
		var target = "#auto.fn."+common.crc32String(customFilter.toString())
		if (this.customIndexes[target]) {
			this.customIndexes[target][keyword];
		}
	}
	indexes = indexes || this._tempIndexes || {};
	return indexes[keyword];
}

/**
 * Clear temporary index
 * @param {Function|String} [target] - Target custom index. If empty this function will remove all indexes.
 */
Trans.prototype.clearTemporaryIndexes = function(target) {
	if (target) {
		if (typeof target == "string") {
			if (this.customIndexes[target]) delete this.customIndexes[target];
		} else if (typeof target == "function") {
			target = "#auto.fn."+common.crc32String(target.toString())
			if (this.customIndexes[target]) delete this.customIndexes[target];
		}
	} else {
		delete this.customIndexes
	}
	this._tempIndexes = undefined;
}

/**
 * Find the row index by text
 * @param {String} index - Index to look for
 * @param {String} fileId - The file ID to process
 * @returns {Number} The row index of the key string
 */
 Trans.prototype.findIdByIndex = function(index, fileId) {
	if (trans.data == null || trans.data == '' || typeof trans.data == 'undefined') return false;
	
	if (typeof fileId == 'undefined') {
		if (typeof trans.indexIds == 'undefined') trans.buildIndex(); 
		if (typeof trans.indexIds[index] == "undefined") return false;
		return trans.indexIds[index];
	} else {
		if (trans.project.files[fileId].indexIsBuilt == false) trans.buildIndex(fileId); 
		if (typeof trans.project.files[fileId].indexIds == 'undefined') trans.buildIndex(fileId); 
		//console.log(trans.project.files[fileId].indexIds);
		if (typeof trans.project.files[fileId].indexIds[index] == 'undefined') return false;
		return trans.project.files[fileId].indexIds[index];
	}
}

Trans.prototype.getClearOnNextHumanInteract = function(key) {
	this._clearOnNextHumanInteract = this._clearOnNextHumanInteract || {};
	if (!key) return this._clearOnNextHumanInteract;
	this._clearOnNextHumanInteract[key] = this._clearOnNextHumanInteract[key] || {};
	return this._clearOnNextHumanInteract[key];
}

/**
 * Get the row ID by text. Case insensitive.
 * @param {String} str - text to look for
 * @param {String} fileId - File to look for
 * @returns {Number} Row index
 */
 Trans.prototype.getRowIdByTextInsensitive = function(str, fileId) {
	if (this.data == null || this.data == '' || typeof this.data == 'undefined') return false;
	if (!fileId) throw "second argument fileId is required!"
	var thisIndex = this.getClearOnNextHumanInteract(`insensitiveIndex_${fileId}`);

	var data = this.getData(fileId)
	var makeInsensitive = (txt) => {
		if (!txt) return "";
		return common.trimRightParagraph(txt).toLowerCase();
	}
	if (empty(thisIndex)) {
		// building index
		for (var rowId=0; rowId<data.length; rowId++) {
			var thisKey = makeInsensitive(data[rowId][this.keyColumn]);
			thisIndex[thisKey] = rowId;
		}
	}
	return thisIndex[makeInsensitive(str)]
}

/**
 * Copy imported translation into the current objects with the same id and same row index
 * @param  {trans.project|trans.project.files} obj
 * @param  {Number} [targetColumn=1] - Target column
 * @param  {Object} options
 * @param  {String[]} options.files - File IDs to process
 * @param  {'lineByLine'|'rowByRow'} [options.mode=lineByLine] - Translation mode
 * @param  {'translated'|'untranslated'|'both'} [options.fetch=translated] - Fetch translated only, or untranslated only, or both
 * @param  {String[]} [options.filterTag] - Tags filter
 * @param  {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param  {Boolean} [options.overwrite=false] - Whether to overwrite or not if the destination cell is not empty
 * @param  {Number} options.targetColumn
 */
 Trans.prototype.copyTranslationToRow = function(obj, targetColumn, options) {
	console.log("copyTranslationToRow", arguments);
	if (typeof obj == 'undefined') return false;
	if (typeof obj.files !== 'undefined') obj = obj.files;
	targetColumn 			= targetColumn || options.targetColumn || 1;
	options = options||{};
	options.files 			= options.files||[];
	options.mode 			= options.mode||""; 
	options.fetch 			= options.fetch||"";
	options.filterTag 		= options.filterTag || [];
	options.filterTagMode 	= options.filterTagMode || "";
	options.overwrite		= options.overwrite || false;
	if (Array.isArray(options.files) == false) options.files = [options.files];
	if (options.files.length == 0) {
		for (let file in obj) {
			options.files.push(file);
		}
	}
	
	for (var i=0; i<options.files.length; i++) {
		let file = options.files[i];
		console.log("Handling", file);
		if (Boolean(obj[file]) == false) continue;
		var objWithSameId = trans.getObjectById(file);

		for (var row=0; row<obj[file].data.length; row++) {
			var thisRow = obj[file].data[row]
			if (Boolean(thisRow[0]) == false) continue;
			if (empty(objWithSameId.data[row])) continue;
			if (options.overwrite == false && Boolean(objWithSameId.data[row][targetColumn])) continue;

			var thisTranslation = trans.getTranslationFromRow(thisRow);
			if (Boolean(thisTranslation) == false) thisTranslation = thisRow[0];
			objWithSameId.data[row][targetColumn] = thisTranslation;
		}
	}
}

/**
 * Generates context translation pair
 * @param {trans.project|trans.project.files} obj  - trans.project or trans.project.files format
 * @param {Object} options 
 * @param  {String[]} options.files - File IDs to process
 * @param  {'lineByLine'|'rowByRow'} [options.mode=lineByLine] - Translation mode
 * @param  {'translated'|'untranslated'|'both'} [options.fetch=translated] - Fetch translated only, or untranslated only, or both
 * @param  {String[]} [options.filterTag] - Tags filter
 * @param  {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @returns {Object} translation table object {key: "translation strings"}
 */
 Trans.prototype.generateContextTranslationPair = function(obj, options) {
	// return translation table object
	// Normal mode :
	// {key: "translation strings"}

	// 
	// only on lineByLine mode : 
	// options.fetch
	// translated, untranslated, both
	// default: translated
	console.log("Entering trans.generateTranslationTable");
	if (typeof obj == 'undefined') return false;
	if (typeof obj.files !== 'undefined') obj = obj.files;
	console.log("obj files inside trans.generateTranslationTable :");
	console.log(obj);
	var result = {};
	options = options||{};
	options.files 			= options.files||[];
	options.mode 			= options.mode||""; 
	options.fetch 			= options.fetch||"";
	options.filterTag 		= options.filterTag || [];
	options.filterTagMode 	= options.filterTagMode || "";

	if (Array.isArray(options.files) == false) options.files = [options.files];
	if (options.files.length == 0) {
		for (let file in obj) {
			options.files.push(file);
		}
	}
	
	console.log("Worked option files : ");
	console.log(options.files);	

	for (let i=0; i<options.files.length; i++) {
		let file = options.files[i];
		if (Boolean(obj[file]) == false) continue;
		for (var row=0; row<obj[file].context.length; row++) {
			if (Boolean(obj[file].context[row]) == false) continue;
			if (Boolean(obj[file].data[row]) == false) continue;
			var thisTranslation = trans.getTranslationFromRow(obj[file].data[row]);
			if (Boolean(thisTranslation) == false) thisTranslation = obj[file].data[row][trans.keyColumn];
			if (Boolean(thisTranslation) == false) continue;

			for (var contextId=0; contextId<obj[file].context[row].length; contextId++) {
				var thisContextKey = obj[file].context[row][contextId];
				result[thisContextKey] = thisTranslation;
			}
		}
	}
	console.log("translation table collection is : ");
	console.log(result);
	return result;	
}

/**
 * Generates context translation table row by row mode
 * @param {trans.project|trans.project.files} obj  - trans.project or trans.project.files format
 * @param {Object} options 
 * @param  {String[]} options.files - File IDs to process
 * @param  {'lineByLine'|'rowByRow'} [options.mode=lineByLine] - Translation mode
 * @param  {'translated'|'untranslated'|'both'} [options.fetch=translated] - Fetch translated only, or untranslated only, or both
 * @param  {String[]} [options.filterTag] - Tags filter
 * @param  {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param  {Boolean} [options.caseSensitive=false] - Whether case sensitive or not
 * @returns {Object} translation table object {key: "translation strings"}
 */
 Trans.prototype.generateTranslationTable = function(obj, options) {
	// return translation table object
	// Normal mode :
	// {key: "translation strings"}
	// Line by Line mode :
	// {keyLine1: "translation line1"}
	// options.mode = default||lineByLine
	// 
	// only on lineByLine mode : 
	// options.fetch
	// translated, untranslated, both
	// default: translated
	console.log("Entering trans.generateTranslationTable");
	if (typeof obj == 'undefined') return false;
	if (typeof obj.files !== 'undefined') obj = obj.files;
	console.log("obj files inside trans.generateTranslationTable :");
	console.log(obj);
	var result = {};
	options = options||{};
	options.files = options.files||[];
	options.caseSensitive = options.caseSensitive||false; // aware of extension
	options.mode = options.mode||""; 
	options.fetch = options.fetch||"";
	options.filterTag = options.filterTag || [];
	options.filterTagMode = options.filterTagMode || "";

	if (Array.isArray(options.files) == false) options.files = [options.files];
	if (options.files.length == 0) {
		for (let file in obj) {
			options.files.push(file);
		}
	}
	
	console.log("Worked option files : ");
	console.log(options.files);
	
	// LINE BY LINE MODE
	if (options.mode.toLowerCase() == "linebyline") {
		for (let i=0; i<options.files.length; i++) {
			let file = options.files[i];
			console.log(file);
			for (var row=0; row<obj[file].data.length; row++) {
				if (Boolean(obj[file].data[row][0]) == false) continue;

				if (options.filterTagMode == "blacklist") {
					if (this.hasTags(options.filterTag, row, file)) continue;
				} else if (options.filterTagMode == "whitelist") {
					if (!this.hasTags(options.filterTag, row, file)) continue;
				}

				let thisTranslation = trans.getTranslationFromRow(obj[file].data[row]);
				if (options.fetch == "untranslated") {
					if (Boolean(thisTranslation) == true) continue;
				} else if (options.fetch == "both") {
					// do nothing
				} else {
					if (Boolean(thisTranslation) == false) continue;
				}
				
				/*
				var splitedIndex = obj[file].data[row][0].split("\n").
						map(function(input) {
							input = input||"";
							return input.replace(/\r/g, "")
						});;
				*/
				// I thin'k no need to strip \r from key element
				var splitedIndex = obj[file].data[row][0].split("\n");
				
				thisTranslation = thisTranslation||"";
				var splitedResult = thisTranslation.split("\n").
						map(function(input) {
							input = input||"";
							return input.replaceAll(/\r/g, "")
						});						
						
				for (var x=0; x<splitedIndex.length; x++) {
					if (options.fetch == "untranslated") {
						if (Boolean(splitedResult[x]) == true) continue;
					} else if (options.fetch == "both") {
						// do nothing
					} else {
						if (Boolean(splitedResult[x]) == false) continue;
					}
					result[splitedIndex[x]] = splitedResult[x]||"";
				}

			}
		}	
		
		console.log("translation table collection is : ");
		console.log(result);
		return result;
	}
	
	
	// DEFAULT MODE!
	for (let i=0; i<options.files.length; i++) {
		let file = options.files[i];
		//console.log("Handling obj file : ", file, obj[file]);
		if (Boolean(obj[file]) == false) continue;
		for (let row=0; row<obj[file].data.length; row++) {
			if (Boolean(obj[file].data[row][0]) == false) continue;
			let thisTranslation = trans.getTranslationFromRow(obj[file].data[row]);
			if (Boolean(thisTranslation) == false) continue;
			result[obj[file].data[row][0]] = thisTranslation;
		}
	}
	console.log("translation table collection is : ");
	console.log(result);
	return result;
	
}

/**
 * Generate translation table line by line mode
 * @param  {trans.project|trans.project.files} obj
 * @param  {Number} [targetColumn=1] - Target column
 * @param  {Object} options
 * @param  {String[]} options.files - File IDs to process
 * @param  {'lineByLine'|'rowByRow'} [options.mode=lineByLine] - Translation mode
 * @param  {'translated'|'untranslated'|'both'} [options.fetch=translated] - Fetch translated only, or untranslated only, or both
 * @param  {String[]} [options.filterTag] - Tags filter
 * @param  {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param  {Boolean} [options.overwrite=false] - Whether to overwrite or not if the destination cell is not empty
 * @param  {Boolean} [options.ignoreTranslated=false] - Whether to skip processing or not if the row already has translation
 * @param  {Number} options.targetColumn
 * @returns {Object} translation table object {key: "translation strings"}
 */
 Trans.prototype.generateTranslationTableLine = function(obj, options) {
	// return translation table object
	// Normal mode :
	// {key: "translation strings"}
	// Line by Line mode :
	// {keyLine1: "translation line1"}
	// options.mode = default||lineByLine
	// 
	// only on lineByLine mode : 
	// options.fetch
	// translated, untranslated, both
	// default: translated
	console.log("Entering trans.generateTranslationTableLine", obj, options);
	if (typeof obj == 'undefined') return false;
	if (typeof obj.files !== 'undefined') obj = obj.files;
	console.log("obj files inside trans.generateTranslationTable :");
	console.log(obj);
	var result = {};
	options = options||{};
	options.files 				= options.files||[];
	options.caseSensitive 		= options.caseSensitive||false; // aware of extension
	options.mode 				= options.mode||""; 
	options.fetch 				= options.fetch||"";
	options.keyColumn 			= options.keyColumn||0;
	options.filterTag 			= options.filterTag || [];
	options.filterTagMode 		= options.filterTagMode || "";
	options.ignoreTranslated 	= options.ignoreTranslated || false;
	options.overwrite			= options.overwrite || false;
	options.collectAddress      = options.collectAddress || false;
	options.targetColumn;

	try {
		options.filterLanguage = options.filterLanguage||this.getSl()||"ja"; // japanese
	} catch(e) {
		options.filterLanguage = "ja"; // japanese
	}
	options.ignoreLangCheck = options.ignoreLangCheck||false;
	
	console.log("ignore language check?");
	console.log(options.ignoreLangCheck);
	
	if (Array.isArray(options.files) == false) options.files = [options.files];
	if (options.files.length == 0) {
		for (let file in obj) {
			options.files.push(file);
		}
	}
	
	console.log("Worked option files : ");
	console.log(options.files);
	const addresses = {}
	
	for (let i=0; i<options.files.length; i++) {
		let file = options.files[i];
		console.log("fetching translatable data from:", file);
		for (let row=0; row<obj[file].data.length; row++) {
			if (Boolean(obj[file].data[row][options.keyColumn]) == false) continue;
			
			if (options.filterTagMode == "blacklist") {
				if (this.hasTags(options.filterTag, row, file)) continue;
			} else if (options.filterTagMode == "whitelist") {
				if (!this.hasTags(options.filterTag, row, file)) continue;
			}

			if (options.ignoreTranslated) {
				if (this.rowHasTranslation(obj[file].data[row], options.keyColumn)) continue;
			}

			if (options.overwrite == false && options.targetColumn) {
				if (obj[file].data[row][options.targetColumn]) continue;
			}

            console.log("Reached here!");
			var thisTranslation = trans.getTranslationFromRow(obj[file].data[row], options.keyColumn, [0]);
            console.log("current translation", thisTranslation);
			

			// I think no need to strip \r from key element
			var splitedIndex = obj[file].data[row][options.keyColumn].split("\n");
			
			thisTranslation = thisTranslation||"";
			var splitedResult = thisTranslation.split("\n").
					map(function(input) {
						input = input||"";
						return input.replaceAll(/\r/g, "")
					});						
					
			for (var x=0; x<splitedIndex.length; x++) {
				// if (options.ignoreWhitespace) {
				// 	if (!splitedResult[x]) continue;
				// 	if (!splitedResult[x].trim()) continue;
				// }
				if (options.fetch == "untranslated") {
					if (Boolean(splitedResult[x]) == true) continue;
				} else if (options.fetch == "both") {
					// do nothing
				} else {
					if (Boolean(splitedResult[x]) == false) continue;
				}
				
				if (options.ignoreLangCheck == false) {
					// skip collecting data if language doesn't match options.filterLanguage
					if (common.isInLanguage(splitedIndex[x], options.filterLanguage) == false) continue; 
				}
				
				result[splitedIndex[x]] = splitedResult[x]||"";
				if (options.collectAddress) {
					addresses[splitedIndex[x]] ||= [];
					addresses[splitedIndex[x]].push({
                           line:x,
                           file:file,
                           row:row,
                           rowObj:obj[file].data[row]
                    })
				}
			}

		}
	}	
	
	console.log("result is : ");
	console.log(result);
    console.log("addresses:", addresses);
    if (options.collectAddress) {
        return {
			pairs: result,
			addresses: addresses
        }
    }
	return result;
	
}

/**
 * generate translation pair from strings
 * @param {String|String[]} input - input can be string or array of string
 * @param {TranslatorEngine} transEngine - Translator engine 
 * @param {Object} options 
 * @param {String} [options.filterLanguage=this.getSl()] 
 * @param {Boolean} [options.ignoreLangCheck=boolean] 
 * @param {Boolean} [options.ignoreLangCheck=boolean] 
 * @returns {Object} - include{'source text':'translation'},
 *			 exclude{'filtered text':''}
 */
 Trans.prototype.generateTranslationTableFromStrings = function(input, transEngine, options) {
	/*
	input can be string or array of string
	generate translation pair from strings
	return : include{'source text':'translation'},
			 exclude{'filtered text':''}
	*/
	options = options||{};
	try {
		options.filterLanguage = options.filterLanguage||this.getSl()||"ja"; // japanese
	} catch(e) {
		options.filterLanguage = "ja"; // japanese
	}
	options.ignoreLangCheck = options.ignoreLangCheck||false;

	var ignoreLangCheck
	try {
		ignoreLangCheck = options.ignoreLangCheck || transEngine.getOptions("ignoreLangCheck");


	} catch (e) {
		ignoreLangCheck = false;
	}
	
	if (typeof input == 'string') input = [input];
	
	var result = {
		include:{},
		exclude:{}
	};
	
	if (options.mode == "rowByRow") {
		for (let i=0; i<input.length; i++) {
			if (typeof input[i] != 'string') continue;
			if (input[i].length < 1) continue;
			
			if (!ignoreLangCheck) {
				// skip collecting data if language doesn't match options.filterLanguage
				if (common.isInLanguage(input[i], options.filterLanguage) == false) {
					result.exclude[input[i]] = input[i];
					continue; 
				}
			}
			result.include[input[i]] = "";
		}
	} else {
		for (let i=0; i<input.length; i++) {
			if (typeof input[i] != 'string') continue;
			if (input[i].length < 1) continue;
			var splitedIndex = input[i].replaceAll("\r", "").split("\n");
			
			for (var x=0; x<splitedIndex.length; x++) {
				if (options.ignoreWhitespace) {
					if (!splitedIndex[x]) continue;
					if (!splitedIndex[x].trim()) continue;
				}
				if (ignoreLangCheck) {
					// skip collecting data if language doesn't match options.filterLanguage
					if (common.isInLanguage(splitedIndex[x], options.filterLanguage) == false) {
						result.exclude[splitedIndex[x]] = splitedIndex[x];
						continue; 
					}
				}
				result.include[splitedIndex[x]] = "";
			}
		}
	}
	
	return result;
}

/**
 * generate translation table from translation result
 * @param {String[]} keywordPool - ["keyword1", "keyword2", ... ]
 * @param {String[]} translationPool - ["translationOfKeyword1", "translationOfKeyword2", ...]
 * @param {Object} defaultTrans - Default object
 * @returns {Object} {"keyword1":"translationOfKeyword1", "keyword2":"translationOfKeyword2", ...}
 */
 Trans.prototype.generateTranslationTableFromResult = function(keywordPool, translationPool, defaultTrans) {
	/*
		generate translation table from translation result
		
		keywordPool = ["keyword1", "keyword2", ... ]
		translationPool = ["translationOfKeyword1", "translationOfKeyword2", ...]
		
		result :
		translationTable
		
		{"keyword1":"translationOfKeyword1", "keyword2":"translationOfKeyword2", ...};
	*/
	//console.log("generateTranslationTableFromResult Default translation:", arguments);
	var result = defaultTrans || {};
	for (var i=0; i<keywordPool.length; i++) {
		if (typeof translationPool[i] == 'undefined') continue;
		result[keywordPool[i]] = translationPool[i];
	}
	//console.log(JSON.stringify(result, undefined, 2))
	return result;
}

/**
 * Generates a translation table for the selected cells based on the provided parameters.
 * @param {Array} [currentSelection] - The current selection of cells. If not provided, it defaults to the selected range in the grid.
 * @param {string} [fileId] - The file ID. If not provided, it defaults to the currently selected file ID.
 * @param {Object} [options] - Additional options for generating the translation table.
 * @returns {Object} - The generated translation table.
 */
Trans.prototype.generateSelectedTranslationTable = function(currentSelection, fileId, options) {
	currentSelection = currentSelection||this.grid.getSelectedRange()||[[]];
	fileId = fileId || this.getSelectedId();
	options = options || {};
	var selectedCells = common.gridSelectedCells(currentSelection);
	var thisData = this.getData(fileId)
	var transTable 	= {};
	for (var i=0; i<selectedCells.length; i++) {
		var row = selectedCells[i].row
		var col = selectedCells[i].col
		var origText = thisData[row][this.keyColumn];
		transTable[origText] = transTable[origText] || [];
		transTable[origText].push({
			col:col,
			row:row,
			value:thisData[row][col],
			file:fileId
		})
	}

	return transTable;
}


/**
 * Word wrap a text
 * @param  {String} str
 * @param  {String[]} [rowTags=[]] - List of tags of the current row
 * @param  {String[]} [wordWrapByTags=[]] - List of tags to filter
 * @param  {String} [lineBreak="\n"] - Line break character
 * @returns {String} Word wrapped text
 */
 Trans.prototype.wordWrapText = function(str, rowTags=[], wordWrapByTags=[], lineBreak="\n") {
	//console.log("wordWrap", arguments);
	if (empty(wordWrapByTags)) return str;
	if (empty(rowTags)) return str;
	if (window.langTools?.isCJK(this.getTl())) {
		for (let i=0; i<wordWrapByTags.length; i++) {
			let intersect = common.arrayIntersect(rowTags, wordWrapByTags[i].tags);
			if (intersect.length>0) return common.wordwrapLocale(str, wordWrapByTags[i].maxLength, this.getTl(), lineBreak);
		}
	}
	for (let i=0; i<wordWrapByTags.length; i++) {
		let intersect = common.arrayIntersect(rowTags, wordWrapByTags[i].tags);
		if (intersect.length>0) return common.wordwrap(str, wordWrapByTags[i].maxLength, lineBreak);
	}
	return str;
}


/**
 * Translation Data object generated by trans.getTranslationData
 * @typedef {Object} TranslationData
 * @property {Object} TranslationData.info - Information header of the translation pairs
 * @property {Object} TranslationData.translationData - Translation data
 * @property {Object} TranslationData.translationData[file] - list of translation pairs grouped by the file id
 * @property {Object} TranslationData.translationData[file].info - Information of the current file
 * @property {Object} TranslationData.translationData[file].translationPair - Key value pair of original text and translation
 * @example
 * {
  "info": {
    "filterTag": [],
    "filterTagMode": ""
  },
  "translationData": {
    "data/Actors.json": {
      "info": {},
      "translationPair": {
		  "key text": "translation",
		  "key text2": "translation2",
	  }
    },
    "data/Animations.json": {
      "info": {},
      "translationPair": {
		  "key text": "translation",
		  "key text2": "translation2",
	  }
    }
  }
}
 */

/**
 * Generate Translation Data object
 * @param  {Trans} [transData=trans.getSaveData()] - instance of Trans object
 * @param  {Object} [options]
 * @param  {Object} [options.keyCol=0] - Key column of the table
 * @param  {Object} [options.groupIndex] - index added for the translation pair prefixes
 * @param  {String} [options.groupBy=path] - Group by what key. Default is path. use "id" to group by the key instead.
 * @param  {Object} [options.options]
 * @param  {String[]} [options.filterTag] - Tags filter
 * @param  {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param  {Object} [options.wordWrapByTags]
 * @returns {TranslationData} Translation information
 * @fires onGenerateTranslationData
 */
 Trans.prototype.getTranslationData = function(transData, options) {
	/*
		Generate Translation Pair Advanced
		
		Generate translation pair from project file
		It will use relPath for group key
		result :
		{
			info : {
				groupLevel : 0 // integer
			},
			translationData: {
				[groupName] : {
					info: {
						
					},
					translationPair : {
						"key" : "translation"
						"group[separator]key" : "translation"
						
					}
				}
			}
			
		}		
		
	*/
	options = options || {}
	options.keyCol 		= options.keyCol|| 0;
	options.groupIndex 	= options.groupIndex||undefined; // index added for the translation pair prefixes
	options.groupBy 	= options.groupBy || "path";
	options.objectGrouping = options.objectGrouping || false // if true child translation pair will be under a sub-object inside translationPair object
	options.includeTagsInfo	= options.includeTagsInfo || false;
	transData 			= transData||trans.getSaveData()	
	transData 			= JSON.parse(JSON.stringify(transData));
	transData.project 	= transData.project||{}
	transData.project.files = transData.project.files||{};
	// fix for filtertag mode inside options.options
	// this happens in export mode
	options.options 		= options.options || {};
	options.filterTag 		= options.filterTag||options.options.filterTag||[];
	options.filterTagMode 	= options.filterTagMode||options.options.filterTagMode||""; // whitelist or blacklist
	options.wordWrapByTags 	= options.wordWrapByTags || options.options.wordWrapByTags || transData.project.options.wordWrapByTags;
	options.useSelectedFiles ??= true; 
	options.disableEvent 	||= false; 

	var contextSeparator = options.contextSeparator || "\n"

	var autofillFiles = [];
	if (options.useSelectedFiles) {
		var checkbox = $(".fileList .data-selector .fileCheckbox:checked");
		for (var i=0; i<checkbox.length; i++) {
			autofillFiles.push(checkbox.eq(i).attr("value"));
		}
	}

	options.files = options.files||autofillFiles||[];

	if (options.groupBy == "id") {
		// generating id based on key index
		for (let fileId in transData.project.files) {
			if (!transData.project.files[fileId]) continue;
			transData.project.files[fileId].id = fileId;
		}
	}

	var transGroup = {};
	var info = {
		filterTag  		: options.filterTag,
		filterTagMode 	: options.filterTagMode
	};
	if (!empty(options.wordWrapByTags)) {
		info.wordWrapByTags = options.wordWrapByTags
	}

	for (let fileId in transData.project.files) {
		var thisFiles = transData.project.files[fileId];
		thisFiles.data = thisFiles.data||[[]];
		thisFiles.tags = thisFiles.tags||[];
		if (options.files.length > 0) {
			if (options.files.includes(fileId) == false) continue;
		}
		
		var thisData = {
			info:{
				groupLevel : thisFiles['groupLevel']
			},
			translationPair : {}
		}

		transGroup[thisFiles[options.groupBy]] = transGroup[thisFiles[options.groupBy]] || thisData;
		for (var row=0; row<thisFiles.data.length; row++) {
			if (Boolean(thisFiles.data[row]) == false) continue;
			if (Boolean(thisFiles.data[row][options.keyCol]) == false) continue;
			var thisTag = thisFiles.tags[row] || [];
			if (options.filterTagMode !== "") {
				var intersect = options.filterTag.filter(value => thisTag.includes(value));
				if (options.filterTagMode == "whitelist") {
					if (intersect.length == 0) continue;
				} else { // other than whitelist always assume blacklist
					if (intersect.length > 0) continue;
				}
			} 
			
			try {
				var originalWord 	= thisFiles.data[row][options.keyCol] = thisFiles.data[row][options.keyCol] || "";
				var thisTranslation = trans.getTranslationFromRow(thisFiles.data[row], options.keyCol);
				var transByContext 	= ui.translationByContext.generateContextTranslation(row, fileId, originalWord);

				if ((Boolean(thisTranslation) == false) && transByContext.length == 0) continue
				//console.log("Group result by a key in trans.project.files");
				if (transByContext.length > 0) transGroup[thisFiles[options.groupBy]].translationPair = Object.assign(transGroup[thisFiles[options.groupBy]].translationPair, transByContext.translation)
				
				if (options.objectGrouping) {
					if (thisFiles[options.groupIndex]) {
						//assign to sub object
						//console.log("assigning to child object",thisFiles[options.groupIndex]);
						transGroup[thisFiles[options.groupBy]].translationPair[thisFiles[options.groupIndex]] = transGroup[thisFiles[options.groupBy]].translationPair[thisFiles[options.groupIndex]] || {};
						if ((Boolean(thisTranslation) !== false)) transGroup[thisFiles[options.groupBy]].translationPair[thisFiles[options.groupIndex]][originalWord] = trans.wordWrapText(thisTranslation, thisTag, options.wordWrapByTags);
					} else {
						//direct translation string
						if ((Boolean(thisTranslation) !== false)) transGroup[thisFiles[options.groupBy]].translationPair[originalWord] = trans.wordWrapText(thisTranslation, thisTag, options.wordWrapByTags);
					}
	
				} else {
					var thisKey = thisFiles[options.groupIndex] ? thisFiles[options.groupIndex]+contextSeparator+originalWord : originalWord
					if ((Boolean(thisTranslation) !== false)) transGroup[thisFiles[options.groupBy]].translationPair[thisKey] = trans.wordWrapText(thisTranslation, thisTag, options.wordWrapByTags);
				}


			} catch (e) {
				console.log("Error when processing", fileId, "row", row, thisFiles.data[row][options.keyCol]);
				throw(e);
			}
			//console.log("at the end : ", transGroup[thisFiles[options.groupBy]]);
		}
	}
	
	/**
	 * @event Trans#onGenerateTranslationData
	 * @param  {Object} options
	 * @param  {Object} options.info
	 * @param  {Object} options.translationData
	 */
	if (!options.disableEvent) {
		this.trigger("onGenerateTranslationData", {
			info:info,
			translationData:transGroup
		});
	}

	return  {
		info:info,
		translationData:transGroup
	}
}


/**
 * Build context from parameter
 * @param {Object} parameter - Object paramter
 * @returns {string} context string
 */
 Trans.prototype.buildContextFromParameter = function(parameter) {
	return parameter['VALUE ID']+"/"+consts.eventCode[trans.gameEngine][parameter['EVENT CODE']];
}

// =====================================================================
// DATA VALIDATION & MANIPUlATION & PROJECT CREATION DATA INITIALIZATION
// =====================================================================

/**
 * Get the staging path of the current project
 * @param {Trans} [transData] - Trans Object to identify the staging path
 * @returns {String|undefined} A full path to the stagging directory. Return undefined when fail
 * @since 4.4.4
 */
Trans.prototype.getStagingPath = function(transData) {
	try {
		transData ||= this;
		return nwPath.resolve(transData.project.cache.cachePath)
	} catch (e) {
		return;
	}
}

/**
 * Retrieves the staging data path based on the provided Trans data.
 * @param {any} transData - The Trans data.
 * @returns {string} - The staging data path.
 */
Trans.prototype.getStagingDataPath = function(transData) {
	var defaultBaseData = engines.current().getProperty("stagingDataPath") || "data";
	return nwPath.join(this.getStagingPath(transData), defaultBaseData);
}

/**
 * Update gameInfo.json at staging location
 * @param {Trans|undefined} [transData] - Trans data
 */
Trans.prototype.updateStagingInfo = async function(transData) {
	transData ||= this;
	const stagingInfoFile = nwPath.join(this.getStagingPath(transData)||"", "gameInfo.json");
	console.log("Staging info:", stagingInfoFile);
	var stagingInfo = {}
	if (await common.isFileAsync(stagingInfoFile)) {
		stagingInfo = JSON.parse(await common.fileGetContents(stagingInfoFile));
	}
	stagingInfo.title = transData.project.gameTitle;
	stagingInfo.engine = transData.project.gameEngine;
	await common.filePutContents(stagingInfoFile, JSON.stringify(stagingInfo, undefined, 2), "UTF8", false);
	return stagingInfo;
}

/**
 * Get the real path to the staging file
 * @param {String|Object} obj - String file ID or object 
 * @since 4.6.29
 * @returns {String} Path to the file
 */
Trans.prototype.getStagingFile = function(obj) {
	if (typeof obj == "string") {
		obj = this.getObjectById(obj)	
	}

	if (!obj) return;
	if (typeof obj !== "object") return;
	if (!obj.path) return;

	var stagintPath = nwPath.join(this.getStagingDataPath(), obj.path);
	return stagintPath;
}

/**
 * Insert cell
 * @param {Number} index 
 * @param {String|null} value 
 */
 Trans.prototype.insertCell = function(index, value) {
	value = value||null;
	common.batchArrayInsert(trans.data, index, value);
	if(typeof trans.project == "undefined") return trans.alert(t("Please open or create a new project first!"));

	for (var file in trans.project.files) {
		if (file == trans.getSelectedId()) continue;
		common.batchArrayInsert(trans.project.files[file].data, index, value);
	}
}

/**
 * Copy column
 * @param {Number} from 
 * @param {Number} to 
 * @param {Object} [project=trans.project] 
 * @param {Object} [options] 
 */
 Trans.prototype.copyCol = function(from, to, project, options) {
	console.log("Copying column");
	console.log(arguments);
	options = options || {};
	project = project || trans.project;
	
	if (typeof project == 'undefined') return console.log("project is undefined");
	if (typeof project.files == 'undefined') return console.log("project.files are undefined");
	
	for (var file in project.files) {
		if (Array.isArray(project.files[file].data) == false) {
			console.log("no data for files "+file);
			continue;
		}
		for (var row in project.files[file].data) {
			project.files[file].data[row][to] = project.files[file].data[row][from];
		}
	}
	
}

/**
 * Check whether the grid is modified or not
 * @param {Boolean} [flag] - If flag is defined, then set the flag
 * @returns {Boolean}
 */
 Trans.prototype.gridIsModified =function(flag) {
	if (typeof flag == 'undefined') return this.unsavedChange;
	
	if (!this.project) {
		this.unsavedChange = false;
		return;
	}

	if (this.unsavedChange !== flag) {
		/**
		 * Triggered when grid is modified
		 * @event Trans#documentModifiedStateChange
		 * @param  {Boolean} flag
		 */
		this.trigger("documentModifiedStateChange", flag);
	} 
	this.unsavedChange = flag;

	//console.trace("Grid is modified");

	var thisId = this.getSelectedId();
	if (Boolean(this.project.files) == false) return false;
	if (!this.project.files[thisId]) return false;
	this.project.files[thisId]["cacheResetOnChange"] = {};
	return this.unsavedChange;
}

/**
 * iteratively select all files and return to the last selection
 */
 Trans.prototype.walkToAllFile = function() {
	console.log($(".fileList .selected"));
	var current = $(".fileList li").index($(".fileList .selected"));
	for (var i=0; i<$(".fileList li").length; i++) {
		$(".fileList li").eq(i).trigger("click");
	}
	$(".fileList li").eq(current).trigger("click");
	
}

/**
 * Move a column to the new index
 * @param {Number} fromIndex - Source column index
 * @param {Number} toIndex - Destination column index
 * @returns {Trans} Instance of trans
 */
 Trans.prototype.moveColumn = async function(fromIndex, toIndex) {
	console.log("trans.moveColumn");
	console.log(arguments);
	if (typeof trans.project == 'undefined') return false;
	if (toIndex > Math.min.apply(null, fromIndex)) {
		console.log("move to rigth");
		toIndex = toIndex-1;
	}
	if (fromIndex[0] == toIndex) return false;
	if (toIndex < Math.max.apply(null, fromIndex)) {
		console.log("move to left");
	}
	ui.showBusyOverlay();
	for (var file in trans.project.files) {
		//if (file == trans.getSelectedId()) continue;
		for (var row=0; row<trans.project.files[file].data.length; row++) {
			trans.project.files[file].data[row] = common.arrayMoveBatch(trans.project.files[file].data[row], fromIndex, toIndex);
		
			// adjusting cellInfo
			this.cellInfo.moveCell(file, row, fromIndex, toIndex);
		}
	}
	//sorting colHeaders
	trans.colHeaders = common.arrayMoveBatch(trans.colHeaders, fromIndex, toIndex);
	//sorting column
	trans.column = common.arrayMoveBatch(trans.column, fromIndex, toIndex);
	

	await common.wait(250)

	trans.grid.destroy();
	trans.initTable();
	ui.hideBusyOverlay();
	trans.renderGridInfo();
	trans.gridIsModified(true);
	return trans;
}


/**
 * Padding data by the length of header column
 * @returns {Trans}
 */
 Trans.prototype.dataPadding = function() {
	// padding data by the length of header column
	if (typeof trans.data == 'undefined') return false;
	console.log("Trans data : ", trans.data);
	for (let i in trans.data) {
		if (Array.isArray(trans.data[i]) == false) trans.data[i] = [];
		if (trans.data[i].length >= trans.colHeaders.length) continue;
		let dif = trans.colHeaders.length - trans.data[i].length;
		if (dif < 0) continue;
		console.log("trans.data[i] : ", trans.data[i]);
		console.log("trans.data : ", trans.data[i].length);
		console.log("dif length : ", dif);
		let padding = Array(dif).fill(null);
		trans.data[i] = trans.data[i].concat(padding);
	}	
	
	if (typeof trans.project == 'undefined') return false;
	
	for (let file in trans.project.files) {
		for (let i in trans.project.files[file].data) {
			if (trans.project.files[file].data[i].length >= trans.colHeaders.length) continue;
			
			let dif = trans.colHeaders.length - trans.project.files[file].data[i].length;
			let padding = Array(dif).fill(null);
			trans.project.files[file].data[i] = trans.project.files[file].data[i].concat(padding);
		}
	}
	trans.refreshGrid();
	return trans;
}


/**
 * Generating header based on the maximum length of the data
 * @param {Trans} [trans=this] - Trans data
 * @param {String} [prefix] 
 * @returns {Object} Column header
 */
 Trans.prototype.generateHeader = function(trans, prefix) {
	// generating header based on the maximum length of the data
	trans 	= trans || this;
	prefix 	= prefix || "";
	var maxLength = 0;
	for (let file in trans.project.files) {
		let currentData = trans.project.files[file].data;
		currentData = currentData||[];
		for (let i=0; i<currentData.length; i++) {
			if (Array.isArray(currentData[i]) == false) continue;
			if (currentData[i].length > maxLength) maxLength = currentData[i].length;
		}
	}
	

	for (let i=trans.colHeaders.length-1; i<maxLength; i++) {
		trans.colHeaders.push(prefix+String.fromCharCode(65+i))
		trans.columns.push({});
	}
	
	return trans.colHeaders;
}

/**
 * Sanitize instance of the Trans data
 * @param {Trans} trans 
 * @returns {Trans}
 */
 Trans.prototype.sanitize = function(trans) {
	trans = trans || this;
	console.log("running trans.sanitize");
	if (typeof trans.project == 'undefined') return false;
	if (typeof trans.project.files == 'undefined') return false;
	
	//var rowPad = JSON.parse(JSON.stringify(this.colHeaders))
	//rowPad.fill(null)
	//console.log("data of rowPad : ", rowPad);
	for (var file in trans.project.files) {
		var currentData 	= trans.project.files[file].data;
		var currentContext 	= trans.project.files[file].context||[];
		var currentTags 	= trans.project.files[file].tags||[];
		var currentParameters = trans.project.files[file].parameters||[];
		var inCache 		= {};
		var newData 		= [];
		var newContext 		= [];
		var newTags 		= [];
		var newParameters	= [];
		
		this.colHeaders = this.colHeaders||[];
		if (Array.isArray(currentData) == false) {
			// if not an array, overwrite with blank array.
			trans.project.files[file].data = [JSON.parse(JSON.stringify(this.colHeaders)).fill(null)];
			continue;
		} else if (currentData.length == 0) {
			trans.project.files[file].data = [JSON.parse(JSON.stringify(this.colHeaders)).fill(null)]; 
			continue;
		}
		
		currentData = currentData||[];
		
		for (var i=0; i<currentData.length; i++) {
			if (typeof currentData[i][0] !== 'string') continue;
			if (currentData[i][0].length < 1) continue;
			
			if (typeof inCache[currentData[i][0]] == 'undefined') {
				newData.push(currentData[i]);
				var row = newData.length - 1;
				if (currentContext[i]) 	newContext[row] = currentContext[i];
				if (currentTags[i]) 	newTags[row] 	= currentTags[i];
				if (currentParameters[i]) 	newParameters[row] 	= currentParameters[i];
				
				inCache[currentData[i][0]] = true;
			}
		}
		trans.project.files[file].data 			= newData;
		trans.project.files[file].context 		= newContext;
		trans.project.files[file].tags 			= newTags;
		trans.project.files[file].parameters 	= newParameters;
	}
	trans.project.isDuplicatesRemoved = true;
	return trans;
}

/**
 * Remove duplicate entries from current trans.data
 * Should be run once on initialization
 * @returns {string[][]} trans.data, two dimensional array of the grid
 */
 Trans.prototype.removeDuplicates = function() {
	console.log("running trans.removeDuplicates");
	var inCache = {};
	var newData = [];
	for (var i=0; i<trans.data.length; i++) {
		if (typeof inCache[trans.data[i][0]] == 'undefined') {
			newData.push(trans.data[i]);
			inCache[trans.data[i]] = true;
		}
	}
	trans.data = newData;
	return trans.data;
}

/**
 * Check whether the key text is exist on a file
 * @param {String} key - key text to find
 * @param {String} fileId - File id to search for
 * @returns {Boolean}
 */
 Trans.prototype.isKeyExistOn = function(key, fileId) {
	if (typeof fileId == 'undefined') fileId = trans.getSelectedId();
	if (typeof trans.findIdByIndex(key, fileId) == 'number') {
		return true;
	} else {
		return false;
	}
}

/**
 * Check whether the key text is exist on current active file
 * @param {String} key - key text to find
 * @returns {Boolean}
 */
 Trans.prototype.isKeyExist = function(key) {
	if (typeof trans.findIdByIndex(key) == 'number') {
		return true;
	} else {
		return false;
	}

}




// ============================================================
// 							TAGGING
// ============================================================

/**
 * Set tags to the preferred row
 * @param {String} file - File Id
 * @param {Number} row - Row index
 * @param {String|String[]} tags - tags to set
 * @param {Object} options 
 * @param {Boolean} [options.append] - Whether to append or to override the tags with the current value 
 * @returns {String[]} The current tags of the preferred row
 */
 Trans.prototype.setTags = function(file, row, tags, options) {
	/* options : {
			append : boolean
		}
	*/
	options = options||{};
	if (Array.isArray(tags) == false) tags = [tags];
	if (typeof this.project == 'undefined') return false;
	if (typeof this.project.files[file] == 'undefined') return false;
	if (typeof row != 'number') return false;
	if (typeof this.project.files[file].tags == 'undefined') this.project.files[file].tags = []; 
	if (Boolean(options.append) == true) {
		this.project.files[file].tags[row] = this.project.files[file].tags[row]||[];
	
		this.project.files[file].tags[row].push.apply(this.project.files[file].tags[row], tags);
		this.project.files[file].tags[row] = this.project.files[file].tags[row].filter((v, i, a) => a.indexOf(v) === i); 
	} else {
		this.project.files[file].tags[row] = tags;
	}
	//this.grid.render();
	return this.project.files[file].tags[row];
	
}

/**
 * Removes one or more tags from the preferred row
 * @param {String} file - File Id
 * @param {Number} row - Row index
 * @param {String|String[]} tags - tags to set
 * @param {Object} options 
 * @returns {String[]} The current tags of the preferred row
 */
 Trans.prototype.removeTags = function(file, row, tags, options) {
	options = options||{};
	file = file||this.getSelectedId();
	
	if (Array.isArray(tags) == false) tags = [tags];
	if (typeof this.project == 'undefined') return false;
	if (typeof this.project.files[file] == 'undefined') return false;
	if (typeof row != 'number') return false;
	if (typeof this.project.files[file].tags == 'undefined') this.project.files[file].tags = []; 
	
	let arr = this.project.files[file].tags[row];
	arr = arr.filter(item => !tags.includes(item))
	
	this.project.files[file].tags[row] = arr;

	return this.project.files[file].tags[row];	
}

/**
 * Removes one or more tags from the preferred row
 * @param {String} file - File Id
 * @param {Number|CellRanges} cellRange - Row index
 * @param {Object} options 
 * @returns {String[]} The current tags of the preferred row
 */
 Trans.prototype.clearTags = function(file, cellRange, options) {
	options = options||{};
	file = file||this.getSelectedId();
	if (typeof this.project == 'undefined') return false;
	if (typeof this.project.files[file] == 'undefined') return false;
	if (typeof cellRange == 'number') {
		this.project.files[file].tags[cellRange] = [];
		return this.project.files[file].tags[cellRange];
	}

	for (let i=0; i<cellRange.length; i++) {
		var cellStart = cellRange[i].start || cellRange[i].from;
		var cellEnd = cellRange[i].end || cellRange[i].to;
		
		if (typeof cellStart.row == 'undefined') continue
		if (typeof cellEnd.row == 'undefined') continue
		
		this.project.files[file].tags = this.project.files[file].tags||[];
		for (var row=cellStart.row; row <= cellEnd.row; row++) {
			this.project.files[file].tags[row] = [];
		}
	}

	return this.project.files[file].tags[row];	
}

/**
 * Removes all tags settings from a file
 * @param {String} file - File Id
 * @param {Object} options 
 * @returns {String[]} The current tags of the preferred file, which is an empty array
 */
 Trans.prototype.resetTags = function(file, options) {
	options = options||{};
	file = file||this.getSelectedId();
	if (typeof this.project == 'undefined') return false;
	if (typeof this.project.files[file] == 'undefined') return false;
	return this.project.files[file].tags = [];
}

/**
 * Append tags into some rows
 * @param {String} file - File Id
 * @param {Number} row - Row index
 * @param {String|String[]} tags - tags to set
 * @returns {String[]} The current tags of the preferred row
 */
 Trans.prototype.appendTags = function(file, row,  tags, options) {
	return this.setTags(file, row,  tags, {append:true, noRefresh:true})
}


/**
 * HOT's CellRange object
 * @typedef {Object} CellRange
 * @property {Object} CellRange.from - The starting cell
 * @property {Object} CellRange.to - The latest selected cell
 * @property {Object} CellRange.highlight - The highlighted cell
 * @example
 * {
  "highlight": {
    "row": 8,
    "col": 2
  },
  "from": {
    "row": 8,
    "col": 2
  },
  "to": {
    "row": 10,
    "col": 2
  }
}
 */

/**
 * Set tag for each CellRange object
 * @param {String|String[]} tagName - Tags to put into
 * @param {CellRange[]} [cellRange=trans.grid.getSelectedRange()] - Cell Range object 
 * @param {String} file - File Id
 * @param {Object} options 
 * @param {Boolean} options.append - Whether to append or to override the current value with the new value
 * @returns {Boolean} True on success
 */
 Trans.prototype.setTagForSelectedRow = function(tagName, cellRange, file, options) {
	/*	from cellRange object or from simple coords
		cellRange :[{"start":{"row":4,"col":2},"end":{"row":4,"col":2}}]
		
		or 
		
		result from trans.grid.getSelectedRange()
	*/
	options = options||{};

	if (typeof options.append == 'undefined' ) options.append = true;
	file = file||this.getSelectedId();
	if (Boolean(cellRange) == false) return false;
	if (Array.isArray(cellRange) == false) return false;
	
	for (let i=0; i<cellRange.length; i++) {
		var cellStart = cellRange[i].start || cellRange[i].from;
		var cellEnd = cellRange[i].end || cellRange[i].to;
		
		if (typeof cellStart.row == 'undefined') continue
		if (typeof cellEnd.row == 'undefined') continue
		
		for (var row=cellStart.row; row <= cellEnd.row; row++) {
			this.setTags(file, row,  tagName, options);
		}
	}
	this.grid.render();
	return true;
}

/**
 * Remove tags with cellRange object
 * @param {String|String[]} tagName - Tags to put into
 * @param {CellRange[]} [cellRange=trans.grid.getSelectedRange()] - Cell Range object 
 * @param {String} file - File Id
 * @param {Object} options 
 * @returns {Boolean} True on success
 */
 Trans.prototype.removeTagForSelectedRow = function(tagName, cellRange, file, options) {
	/*	from cellRange object or from simple coords
		cellRange :[{"start":{"row":4,"col":2},"end":{"row":4,"col":2}}]
		
		or 
		
		result from trans.grid.getSelectedRange()
	*/
	options = options||{};
	options.append = options.append||true;
	file = file||this.getSelectedId();
	if (Boolean(cellRange) == false) return false;
	if (Array.isArray(cellRange) == false) return false;
	
	for (let i=0; i<cellRange.length; i++) {
		var cellStart = cellRange[i].start || cellRange[i].from;
		var cellEnd = cellRange[i].end || cellRange[i].to;
		
		if (typeof cellStart.row == 'undefined') continue
		if (typeof cellEnd.row == 'undefined') continue
		
		for (var row=cellStart.row; row <= cellEnd.row; row++) {
			this.removeTags(file, row,  tagName, options);
		}
	}		
	this.grid.render();
	return true;
}

/**
 * Check whether a row has tags or not
 * @param {String|String[]} tags - Check whether a row has one of these tags
 * @param {Number|Number[]} row - Row(s) to check for
 * @param {String} file - the file Id
 * @returns {Boolean} true on success
 */
 Trans.prototype.hasTags = function(tags, row, file) {
	if (!tags) return false;
	if (typeof row == 'undefined') return false;
	file = file || this.getSelectedId();
	var fileObj = this.getObjectById(file);	
	if (!fileObj) return false;
	if (!fileObj.tags) return false;
	if (!Array.isArray(fileObj.tags[row])) return false;
	if (!Array.isArray(tags)) tags = [tags]
	try {
		for (var i in tags) {
			if (fileObj.tags[row].includes(tags[i])) return true;
		}
		return false;
	} catch (e) {
		return false;
	}
}

/**
 * Display alert
 * @async
 * @param {String} text - Text to display
 * @param {Number} [timeout=3000] - Timeout in miliseconds
 */
 Trans.prototype.alert = async function(text, timeout) {
	timeout = timeout||3000;
	$("#appInfo").attr("title", text);

	return new Promise((resolve, reject) => {
		$("#appInfo").tooltip({
			content:function() {
				return text;
			},
			show: { 
				effect: "slideDown", 
				duration: 200 
			},
			hide: {
				effect: "fade",
				delay: 250
			},
			position: {
				my: "left top",
				at: "left bottom",
				of: "#table"
			},
			open: function( event, ui ) {
				setTimeout(function(){
					$("#appInfo").tooltip("close");
					$("#appInfo").attr("title", "");
					resolve();
				}, timeout);
			}
		});
		
		$("#appInfo").tooltip("open");
	})
}
	
/**
 * Refresh the grid
 * @param {Object} options 
 * @param {Boolean} options.rebuild - Whether or not to rebuild the grid
 * @param {Function} options.onDone - Function to run when the process is done
 */
 Trans.prototype.refreshGrid = function(options) {
	options = options||{};
	options.rebuild = options.rebuild||false;
	
	if(trans.getSelectedId()) {
		trans.data = trans?.project?.files[trans.getSelectedId()].data;
	}
	if (!(trans.data)) trans.data = [[null]];
	if (trans.data.length == 1) {
		if (Array.isArray(trans.data[0]) == false) trans.data = [[null]]
		if (Boolean(trans.data[0][0]) == false) trans.data = [[null]]
	}
	if (trans.data.length == 0) trans.data = [[null]]
	
	if(trans.getSelectedId()) {
		// re assign data in to trans.project.files[trans.getSelectedId()].data
		// in case data is detached;
		trans.project.files[trans.getSelectedId()].data = trans.data;
	}	
	
	if (typeof options.onDone == 'function') {
		trans.grid.addHookOnce('afterRender', function() {
			options.onDone.call(trans.grid);	
		});	
	}
	if (options.rebuild) {
		trans.grid.destroy();
		trans.initTable();
		return true;
	}	
	trans.grid.updateSettings({
		data		: trans.data,
		colHeaders	: trans.colHeaders,
		columns		: trans.columns		
	});	

	// TODO: for performance, should cache this:
	// https://handsontable.com/docs/comments/#basic-example
	trans.loadComments();
}

/**
 * Open note at cell
 * @param {Object} cell 
 * @param {Number} cell.row - Row ID
 * @param {Number} cell.col - Column ID
 */
 Trans.prototype.editNoteAtCell = function(cell) {
	/*
		cel : {row : 0, col: 0}
	*/
	if (typeof cell == 'undefined') {
		try{
			cell = trans.grid.getSelectedRange()[0]['highlight'];
		} catch (error) {
			console.log(error);
			return false;
		}
	}
	var hotComment = trans.grid.getPlugin('comments');	
	hotComment.showAtCell(cell.row, cell.col);
	$(".htCommentTextArea").trigger("click");
	$(".htCommentTextArea").focus();
}	

/**
 * Remove note at selected cell
 * @param {CellRange[]} selection - Selected cell(s)
 */
 Trans.prototype.removeNoteAtSelected = function(selection) {
	console.log("trans.removeNoteAtSelected");
	if (typeof selection == 'undefined') {
		selection = trans.grid.getSelectedRange()
	}
	
	if (Boolean(selection) == false) {
		console.log("no selection were made");
		return false;
	}
	
	console.log(selection);
	var minRow = Math.min(selection[0]['from']['row'], selection[0]['to']['row']);
	var maxRow = Math.max(selection[0]['from']['row'], selection[0]['to']['row']);
	var minCol = Math.min(selection[0]['from']['col'], selection[0]['to']['col']);
	var maxCol = Math.max(selection[0]['from']['col'], selection[0]['to']['col']);
	var hotComment = trans.grid.getPlugin('comments');	
	
	for (var y=minRow; y<=maxRow; y++) {
		for (var x=minCol; x<=maxCol; x++) {
			hotComment.removeCommentAtCell(y, x);
		}
	}
	
}





/**
 * Draw grid's context menu to display translator list
 */
 Trans.prototype.drawGridTranslatorMenu = function() {
	if (!this.translatorContextMenuIsInitialized) {
		console.log("Initializing translator context menu");
		// init translatorContextMenu
		TranslatorEngine.translators = TranslatorEngine.translators || {};
		
		if (empty(TranslatorEngine.translators)) return;

		this.gridContextMenu.translateUsing.submenu.items = [];
		for (var id in TranslatorEngine.translators) {
			(()=> {
				var thisId 		= id;
				var thisEngine 	= TranslatorEngine.translators[id];
				this.gridContextMenu.translateUsing.submenu.items.push({
					key: `translateUsing:${thisId}`,
					name: thisEngine.name,
					callback : ()=> {
						this.translateSelection(undefined, {translatorEngine:thisEngine});
					},
					hidden : ()=> {
						if (this.grid.isColumnHeaderSelected()) return true;
						if (this.grid.isRowHeaderSelected()) return true;
					}
				})
			})()
		}
		this.translatorContextMenuIsInitialized = true;
	}

	return this.gridContextMenu;
}

/**
 * Get the context menu object of the grid
 */
 Trans.prototype.getGridContextMenu = function() {
	
	this.drawGridTranslatorMenu();
	return this.gridContextMenu;
}


Trans.prototype.updateGridContextMenu = function(menu) {
	console.log("updateGridContextMenu", menu);
	if (!this.gridContextMenuIsModified) return;
}

Trans.prototype.modifyGridContextMenu = function(menu) {
	this.gridContextMenuIsModified = true;
}

/**
 * Asynchronously calculates the height of the table based on the provided data.
 * @param {Array} [data=this.data] - The data to calculate the table height from. Defaults to the data stored in the Trans instance.
 * @returns {Promise<number>} - A Promise that resolves with the calculated table height.
 */
Trans.prototype.calculateTableHeights = async function(data = this.data) {
	console.log("Calculating table height");
	const lineHeight = 23;
	const padding = 2*lineHeight;
	if (!data?.length) return lineHeight;
	function calculateTableHeight(table) {
		let totalLines = 0;
	
		// Step 1: Find the maximum number of lines in any cell
		for (let i = 0; i < table.length; i++) {
			if (!table[i]?.length) continue;
			let thisLength = 1;
			for (let j = 0; j < table[i].length; j++) {
				if (!table[i][j]) continue;
				const lines = table[i][j].split('\n').length;
				if (lines > thisLength) {
					thisLength = lines;
				}
			}
			totalLines += thisLength;
		}
	
		// Step 2: Calculate the height
		if (totalLines < table.length) return table.length;
		return totalLines;
	}

	if (data.length < 1000) {
		return (calculateTableHeight(data) * lineHeight) + padding;
	}

	const Handler = require("www/js/CommonWorker.js").Handler
	const workerData= {
        command :"calculateHeights",
        data : data,
        options: {
            logTarget: "ui"
        }
    }
	const handler = new Handler("./www/js/trans.worker.js", workerData)
    const result = await handler.getResult();
	if (!result?.result) return lineHeight;
	return (result.result * lineHeight) + padding;
}

/**
 * Initialization of the grid
 * @param  {Object} options
 */
Trans.prototype.initTable = function(options) {
	options = options||{};
	
	//trans.removeDuplicates();
	var container = document.getElementById('table');
	if (Boolean(container) == false) return false;
	
	Handsontable.dom.addEvent(container, 'blur', function(event) {
		console.log("event", event);
	});	
	Handsontable.debugLevel = common.debugLevel();

	/**
	 * Instance of the Handsontable object
	 * @type {Handsontable}
	 * @see https://handsontable.com/docs/api/core/
	 */
	this.grid = new Handsontable(container, {
		data		: trans.data,
		comments	: true,
		rowHeaders	: true,
		colHeaders	: trans.colHeaders,
		columns		: trans.columns,
		formulas	: false,
		search		: true,
		outsideClickDeselects:false,
		viewportColumnRenderingOffset:15,
		maxCols: Trans.maxCols,
		//autoRowSize: true, // setting this to true will cause jumpy bugs
		// dragToScroll:false,
		// autoRowSize: {syncLimit: 1000},
		autoRowSize: false,
		//trimWhitespace : false,
		//fixedColumnsLeft: 1,
		minSpareRows	: 1,
		filters			: false,
		dropdownMenu	: false,
		autoWrapRow		: true,
		manualColumnMove: true,
		//width: 806,
		//height: 487,	
		manualColumnResize: true,
		//copyPaste		: true,
		copyPaste: { columnsLimit: 15, rowsLimit: 100000 },
		beforeChange: function (changes, source) {
			console.log('beforeChange', arguments);
			console.log(changes);
			console.log(source);
			// changes: [0] = row; [1]=col; [2]=initial value; [3]=changed value
			if (typeof trans.selectedData == 'undefined') return console.warn("unknown selected data");
			
			for (var i=0; i<changes.length; i++) {
				// check if editing the key row
				if (changes[i][1]==0 && changes[i][0] < (trans.grid.getData().length - 1)) {
					console.log(JSON.stringify(changes, undefined, 2));
					trans.alert(t("You should not edit key value"));
					return false;
				}

				if (changes[i][1] != 0) continue; // skip if not first index
				
				// reject if same key is found
				if (Boolean(changes[i][3]) == false) return false;

				if (trans.isKeyExistOn(changes[i][3])) {
					trans.alert(t("Ilegal value")+" <b>'"+changes[i][3]+"'</b> "+t("That value already exist!"));
					return false;
				}
				
				//if (typeof trans.findIdByIndex(changes[i][2]) != 'undefined') delete trans.indexIds[changes[i][2]];
				if (typeof trans.findIdByIndex(changes[i][2]) == 'number') delete trans.indexIds[changes[i][2]];
				//trans.indexIds[changes[i][3]] = changes[i][0];
				trans.selectedData.indexIds[changes[i][3]] = changes[i][0];
			}
			
		},
		afterChange: function (changes, source) {
			//console.log('afterChange');
			//console.log(changes);
			//console.log(source);
			// incoming changes is array;
			// changes[index][0] = row; changes[index][1]=col; 
			// changes[index][2]=previous value; changes[index][3] = new value;
			//console.log("afterChange");
			//console.log(arguments);

			trans.gridIsModified(true);
			if (changes == null) return true;


			if (!trans.getSelectedId()) return true;


			var isChanged = false;
			var progress = trans.project.files[trans.getSelectedId()].progress;
			//var activeCellIndex = 0; //index changes which is active in preview
			for (var cell in changes) {
				if (Array.isArray(changes[cell]) == false) continue;
				if (!trans.data[changes[cell][0]][0]) continue; // do not process if first row is blank or null
				changes[cell][2]=changes[cell][2]||"";
				changes[cell][3]=changes[cell][3]||"";

				// detect current cell from array of changes
				if (changes[cell][0] == trans.lastSelectedCell[0] && changes[cell][1] == trans.lastSelectedCell[1]) {
					//activeCellIndex = cell;
					trans.textEditorSetValue(changes[cell][3]);
				}

				if (changes[cell][2].length>0 && changes[cell][3].length==0) {
					// removing
					if (trans.countFilledCol(changes[cell][0]) == 0) {
						// substracting progress
						if (progress.translated <= 0) continue;
						progress.translated --;
						if (progress.length > 0) {
							progress.percent = progress.translated/progress.length*100;
						} else {
							progress.percent = 0;
						}
						isChanged = true;
					}
				} else if (changes[cell][2].length==0 && changes[cell][3].length>0) {
					// adding
					if (trans.countFilledCol(changes[cell][0]) == 1) {
						// if after adding a value, translation count in this row is exactly 1, than this is new translation for this row
						// adding progress
						if (progress.translated >= progress.length) continue;
						progress.translated ++;
						if (progress.length > 0) {
							progress.percent = progress.translated/progress.length*100;
						} else {
							progress.percent = 0;
						}
						isChanged = true;					
					}
				}
			}
			if (isChanged) {
				var result ={};
				result[trans.getSelectedId()] = progress;
				trans.evalTranslationProgress(trans.getSelectedId(), result);

			}
			trans.trigger("afterCellChange", [changes, source]);

			
		},
		beforeContextMenuShow : (menu) => {
			trans.updateGridContextMenu(menu)
		},
		contextMenu: {
			items: trans.getGridContextMenu()
		},
		
		cells: function (row, col, prop) {
			var cellProperties = {};
			if (col==0) {
				if (typeof trans.data[row] == 'undefined') return cellProperties;
				if (trans.data[row][col] !== null && trans.data[row][col]!=="") {
					cellProperties.readOnly = true;
				}
			}
		
			return cellProperties;
		},
		afterSelection: function(row, column, row2, column2, preventScrolling, selectionLayerLevel) {
			//HOT jumpy when selected, not caused by this
			/**
			 * Triggered after grid selection
			 * @event Trans#beforeProcessSelection
			 * @param  {arguments} arguments
			 */
			console.log("do after selection", arguments);
			trans.trigger("beforeProcessSelection", arguments);
			trans.doAfterSelection(row, column, row2, column2);

			// tried this, not fixed the jumpy:
			//preventScrolling.value = true;
		},
		beforeInit: function() {
			console.log("running before init");
		},
		afterInit: function() {
			trans.buildIndex();
		},
		beforeColumnMove: function(columns, target) {
			console.log("beforeColumnMove");
			
			if (target == 0) return false;
			if (columns.includes(0) == true) return false;
			/*
			console.log(columns);
			console.log(target);
			trans.moveColumn(columns, target);
			*/
			trans.moveColumn(columns, target);
			return true;
		},
		afterColumnMove: function(columns, target) {
			console.log("afterColumnMove");
			
			if (target == 0) return false;
			if (columns.includes(0) == true) return false;
			
			//console.log(columns);
			//console.log(target);
			
			//trans.walkToAllFile();
			return true;
		},
		afterSetCellMeta:  function(row, col, source, val){
			if(source == 'comment'){
				var thisData = trans.getSelectedObject();
				if (!thisData) return true;
				
				if (val == undefined) {
					try {
						delete thisData.comments[row][col];
					}
					catch(err) {
						console.log("unable to delete comment.\nData row:"+row+", col:"+col+" is not exist on trans.project.files[trans.getCurrentID].comments!");
					}					
					return true;
				} else {
					thisData.comments = thisData.comments||[];
					thisData.comments[row] = thisData.comments[row]||[];
					thisData.comments[row][col] = val.value;
					return true;
				}
				
			}
			
		},
		afterRender:function(isForced) {
			if (isForced == true) return true; // natural render

			if ($("#currentCellText").is(":focus")) {
				//console.log("eval focus");
				var visibleCell = ui.getCurrentEditedCellElm();
				if (visibleCell !== false) {
					$("#table .currentCell").removeClass("currentCell");
					visibleCell.addClass("currentCell"); 
				}
			}			
		},
		
		afterRenderer:function(TD, row, column, prop, val, cellProperties) {

			// fired after each cell rendered
			// for performance do not put too much thing here
			TD.setAttribute("data-coord", row+"-"+column);
			if (column > 0) {
				if (trans.getOption("gridInfo")?.isRuleActive) {
					if (trans.getOption("gridInfo")?.viewOrganicCellMarker && trans.isOrganicCell(row, column)) {
						$(TD).addClass("organic"); // organic is obviously viewed
					} else if (trans.getOption("gridInfo")?.viewTrail && trans.isVisitedCell(row, column)) {
						$(TD).addClass("viewed");
					}
				}
			}

			if (column > 0) return; // only render tag for the first column ... the rest is same
			// draw last row
			if (row >= trans.data.length - 1) $(TD).addClass("newKeyField");
			

			//console.log($thisTR);
		},
		afterGetColHeader: function(col, TH) {
			if (col==-1) $(TH).attr("data-role", "tablecorner")
		},
		afterGetRowHeader: function(row, TH) {
			var $thisTH = $(TH)
			var $thisTR = $(TH).closest("tr");
			var $rowInfo = $thisTH.find(".rowInfo");
			if (!$rowInfo.length) {
				let $wrapper = $thisTH.find(">div");
				$rowInfo = $('<span class="rowInfo"></span>').appendTo($wrapper)
			}
			//append info view element


			(()=> {
				// render tags
				if (typeof trans.selectedData == 'undefined') return;
				if (Array.isArray(trans.selectedData.tags) == false) return;
				if (Array.isArray(trans.selectedData.tags[row]) == false) return;
				if ($thisTR.hasClass("tagRendered") == true) return;
				
				let shadowPart = [];
				let borderOffset = 0;
				for (var i=0; i<trans.selectedData.tags[row].length; i++) {
					//console.log($thisTR.find("th"));
					var segmTag = trans.selectedData.tags[row][i];
					if (typeof consts.tagColor[segmTag] !== 'undefined') {
						borderOffset+=consts.tagStripThickness;
						shadowPart.push('inset '+borderOffset+'px 0px 0px 0px '+consts.tagColor[segmTag]);
					}
					$thisTH.addClass("tag-"+trans.selectedData.tags[row][i]);
				}
				if (shadowPart.length != 0) {
					$thisTH.css("box-shadow", shadowPart.join(","));
				}
				$thisTR.addClass("hasTag");
				$thisTR.addClass("tagRendered");
			})()

			// render context translation mark
			if (ui.translationByContext) {
				(()=> {
					if (!trans.rowHasMultipleContext(row, trans.selectedData)) return;
					$thisTH.addClass("hasMC")
					if (ui.translationByContext.rowHasContextTranslation(row, trans.selectedData)) $thisTH.addClass("hasTC");
	
				})()
			}

			// render row info
			(()=>{
				let rowInfoText = trans.getRowInfoText(row);
				if (rowInfoText) {
					$rowInfo.text(rowInfoText)
				} else {
					$rowInfo.text("")
				}
			})()
		},
		afterCreateRow:function(index, amount, source) {
			var thisFile = trans.getSelectedId();
			if (Boolean(thisFile) == false) return false;
			trans.evalTranslationProgress([thisFile]);
		},
		beforePaste:function(data, coords) {
			//console.log("beforePaste triggered");
			//console.log(coords);
			//console.log(arguments);
		},
		afterScrollVertically() {
			// some part of the bottom most part are sometimes clipped, and no further scrolling is possible
			// while some data are hidden beyound scrollable area
			// this is the hackish solution for that.

			// todo ... add 20% scroll ui.bumpGridScroll()
			if (this.skipScrollEvent) return;
			if (this._scrollTimer) clearTimeout(this._scrollTimer) 
			this._scrollTimer = setTimeout(async ()=> {
				var elmHeight = $("#table .wtHolder>*:eq(0)").height();
				if (elmHeight - $("#table .wtHolder").scrollTop() - $("#table .wtHolder").height() <= 0) {
					//console.log("reached bottom");
					if (!$(".newKeyField").visible()) {
						this.render();
						this.skipScrollEvent = true;
						trans.grid.scrollViewportTo(ui.getFirstFullyVisibleRow());
						await common.wait(50);
						this.skipScrollEvent = false;
					}
					//this.loadData(trans.data);
					//if (!$(".newKeyField").visible()) this.addHeight(400);
				}
			}, 200)
			
		}
		
	});
	
	
	Handsontable.dom.addEvent($("#quickFind")[0], 'input', function (event) {
		var search = trans.grid.getPlugin('search');
		var queryResult = search.query(this.value);

		console.log(queryResult);
		trans.grid.render();
	});	

	/**
	 * Handle row AutoRowSize plugin
	 * https://handsontable.com/docs/7.4.2/AutoRowSize.html
	 * Known problem: The grid will jumpy if row size is not complete when user try to click the grid.
	 * 
	 */
	trans.grid.autoRowSize = trans.grid.getPlugin('autoRowSize');
	// hot & walkOnTable custom hook
	trans.grid.isFixedHeight = true;
	trans.grid.view.wt.ignoreAdjustElementSize = false;
	/**
	 * Saves row heights to the local storage for the specified file ID.
	 * @param {string} [fileId=trans.getSelectedId()] - The file ID to save row heights for.
	 * @returns {Promise<void>} - A Promise that resolves when the row heights are saved.
	 */
	const saveRowHeights = async function(fileId=trans.getSelectedId()) {
		if (!trans.grid.getSettings().autoRowSize) return;
		if (trans.grid.isFixedHeight) return;
		if (trans.data?.length < 1000) return
		console.log("Saving cache rowheights for ", fileId);
		if (!trans?.localStorage) return;
		console.log("Calculated row heights length ", trans.grid.autoRowSize.heights.length, "Current row length", trans.data.length);
		if (trans.grid)
		trans.localStorage.set(`${trans.getSelectedId()}?rowHeights`, trans.grid.autoRowSize.heights);
		trans.localStorage.set(`${trans.getSelectedId()}?hiderDimension`, {
			width:trans.grid.view.wt.wtTable.hider.style.width,
			height:trans.grid.view.wt.wtTable.hider.style.height
		})
	}
	// custom hooks
	/**
	 * Loads row heights cache from local storage for the specified column range.
	 * @param {string} [colRange] - The column range to load row heights cache for.
	 * @returns {Promise<boolean>} - A Promise that resolves with a boolean indicating if the cache was loaded successfully.
	 */
	trans.grid.loadRowHeightsCache = async function(colRange) {
		if (!trans.grid.getSettings().autoRowSize) return;
		if (trans.grid.isFixedHeight) return;
		if (trans.data?.length < 1000) return
		console.log("start counting rows height on colRange", colRange)
		if (!trans?.localStorage) return;
		let cache = await trans.localStorage.get(`${trans.getSelectedId()}?rowHeights`);
		console.log("Cache row height is", cache);
		if (!cache?.length) return;
		trans.grid.autoRowSize.heights = await trans.localStorage.get(`${trans.getSelectedId()}?rowHeights`);
		trans.grid.autoRowSize.inProgress = false;
		trans.grid.cachedDimension = await trans.localStorage.get(`${trans.getSelectedId()}?hiderDimension`);
		// hooks on walkontable
		// trans.grid.view.wt.getCachedDimension = function() {
		// 	if (!trans.grid.cachedDimension) return;
		// 	if (trans.grid.cachedDimension) {
		// 		return trans.grid.cachedDimension;
		// 	}
		// }
		return true;
	}
	trans.grid.onCalculateAllRowsHeightStart = async function() {
		ui.tableCornerShowLoading("Counting total rows height. The grid may jumpy while in counting process.");
	}
	trans.grid.onCalculateAllRowsHeightEnd = async function() {
		if (trans.data?.length < 1000) return
		ui.tableCornerHideLoading();
		// cache result
		saveRowHeights();
	}

	trans.off("beforeSelectFile");
	/**
	 * Event handler for the "beforeSelectFile" event.
	 * @param {Event} evt - The event object.
	 * @param {Array} fileIds - An array of file IDs.
	 */
	trans.on("beforeSelectFile", function(evt, fileIds) {
		console.log("%cbeforeSelectFile", "color:pink", fileIds);
		if (!fileIds?.[0]) return;
		if (fileIds[0] == fileIds[1]) return; // no change made
		saveRowHeights(fileIds[0]);
	})

	/**
	 * Sets the fixed table height based on the provided data.
	 * @param {Array} [data=[]] - The data to calculate the table height from.
	 * @returns {Promise<void>} - A Promise that resolves when the table height is set.
	 */
	trans.grid.setFixedTableHeightByData = async function(data = []) {
		if (!trans.grid.isFixedHeight) return;
		if (!data?.length) return;
		// set fixed height first with a large enough data for responsiveness
		// assume every row has 10 line
		let initialHeight = data.length * 22 *10
		const currentId = trans.getSelectedId();
		trans.grid.setFixedTableHeight(initialHeight);

		// begin counting the actual height
		const calculatedHeight = await trans.calculateTableHeights(data);
		if (trans.getSelectedId() !== currentId) return; // exit if the table has changed during processing
		console.log("Setting actual height", calculatedHeight);
		trans.grid.setFixedTableHeight(calculatedHeight);
	}

	/**
	 * Sets the fixed table height.
	 * @param {number|string|boolean} height - The height value. If a string, it should include "px".
	 * @returns {string|undefined} - The set height value, or undefined if height is false.
	 */
	trans.grid.setFixedTableHeight = function(height) {
		if (!trans.grid.isFixedHeight) return;
		if (typeof height == "string" && height.includes("px")) {
			trans.grid.view.wt.wtTable.hider.style.height = height;
			trans.grid.view.wt.ignoreAdjustElementSize = true;
		} else if (typeof height == "number"){
			trans.grid.view.wt.wtTable.hider.style.height = height+"px";
			trans.grid.view.wt.ignoreAdjustElementSize = true;
		} else if (typeof height == "boolean" && height === false) {
			trans.grid.view.wt.ignoreAdjustElementSize = false;
		}
		return trans.grid.view.wt.wtTable.hider.style.height;
	}

	/**
	 * Adds height to the table.
	 * @param {number} num - The amount of height to add.
	 * @returns {number} - The new height after adding the specified amount.
	 */
	trans.grid.addHeight = function(num=0) {
		const currentHeight = parseInt(trans.grid.view.wt.wtTable.hider.style.height);
		trans.grid.view.wt.wtTable.hider.style.height = currentHeight+num+"px";
		return trans.grid.view.wt.wtTable.hider.style.height;
	}

	/**
	 * Gets the height of the table.
	 * @returns {string} - The height of the table.
	 * @see https://handsontable.com/docs/7.4.2/Core.html#getTableHeight
	 * @since 4.7.15
	 * @example
	 * var tableHeight
	 * tableHeight
	 * // returns "500px"
	 */
	trans.grid.getTableHeight = function() {
		return trans.grid.view.wt.wtTable.hider.style.height;
	}

	/**
	 * Check whether the current selection is selected through column header
	 * @returns {Boolean}
	 */
	trans.grid.isColumnHeaderSelected = function() {
		return Boolean($(".htCore thead th.ht__active_highlight").length);
	}	

	/**
	 * Checks if a row header is selected.
	 * @returns {boolean} - True if a row header is selected, false otherwise.
	 */
	trans.grid.isRowHeaderSelected = function() {
		return Boolean($(".htCore tr th.ht__active_highlight").length);
	}	
	
	/**
	 * Inserts a column to the right of the specified position.
	 * @param {string} [colName="New Col"] - The name of the new column.
	 * @param {number} [pos] - The position to insert the column. If not provided, the position will be selected from the currently selected cell.
	 * @returns {void} - This function does not return a value.
	 */
	trans.grid.insertColumnRight = function(colName, pos) {
		if (trans.columns.length >= Trans.maxCols) {
			alert(t("The maximum number of columns for this project has been reached, so you cannot add any more."))
			return;
		}
		colName = colName||"New Col";
		var getCol = pos||trans.grid.getSelected()[0][1];
		var getColSet = trans.columns.length;
		trans.columns.push({});
		common.arrayExchange(trans.columns, getColSet, getCol + 1);
		common.arrayInsert(trans.colHeaders, getCol+1, colName);
		//batchArrayInsert(trans.data, getCol+1, null);
		console.log(trans.columns);
		trans.insertCell(getCol+1, null);

		trans.grid.updateSettings({
			colHeaders:trans.colHeaders
		})
	}
	
	/**
	 * Sets the width of the specified column.
	 * @param {number} colIndex - The index of the column to set the width for.
	 * @param {number} newWidth - The new width value.
	 * @returns {void} - This function does not return a value.
	 */
	trans.grid.setColWidth = function(colIndex, newWidth) {
		if (!trans.columns[colIndex]) return;
		trans.columns[colIndex].width = newWidth;
		trans.refreshGrid();
	}



}

/*
hot2.updateSettings({
  cells: function (row, col) {
    var cellProperties = {};

    if (hot2.getData()[row][col] === 'Nissan') {
      cellProperties.readOnly = true;
    }

    return cellProperties;
  }
});
*/

/**
 * Find a key string, then insert a text into the adjacent cell
 * Starting from 5.1 this function will strips carriage returns from `find`
 * @param {String} find - String to find.
 * @param {String} values - Text to put into
 * @param {Number} columns - Column ID to put the values into
 * @param {Object} indexes - index object or addresses
 * @param {Object} options 
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the existing text on the destination cell or not
 * @param {String[]} [options.files=this.getAllFiles()] - Selected files
 * @param {Boolean} [options.keyColumn=0] - Key column id
 * @param {Boolean} [options.lineByLine=false] - to set the search mode in line by line mode
 * @since 4.7.15
 */
Trans.prototype.findAndInsertWithIndexes = function(find="", value, columns, indexes, options) {
	//console.log("findAndInsertWithIndexes", arguments);
	columns = columns||1;
	options = options||{};
	find ||= "";
	options.overwrite 		= options.overwrite||false;
	options.files 			= options.files||[];
	options.keyColumn		= options.keyColumn || this.keyColumn || 0;
	options.insensitive		= options.insensitive || false;
	options.lineByLine		= options.lineByLine || false;
	options.customFilter;
	indexes 				= indexes || this._tempIndexes;
	
	if (typeof options.ignoreNewLine == "undefined") options.ignoreNewLine = true;
	
	if (options.files.length == 0) { // that means ALL!
		options.files = this.getAllFiles();
	}
	
	var result = {
		keyword	:find,
		count	:0,
		executionTime:0,
		files	:{}
	};
	
	var indexObjects = this.getFromIndexes(find, indexes, options.customFilter);
	//console.log("---indexObjects", indexObjects);
	if (!indexObjects && find.includes("\r")) {
		// RETRY: strip-out carriage returns
		find = find.replaceAll("\r", "");
		result.find = find;
		indexObjects = this.getFromIndexes(find, indexes, options.customFilter);
	}
	if (!indexObjects) return result;
    //console.log("reach here");
	for (let i=0; i<indexObjects.length; i++) {
		let thisAddress = indexObjects[i];
		var file = thisAddress.file;
		var row = thisAddress.row;
		if (options.ignoreNewLine) {
			if (Boolean(this.project.files[file].lineBreak) !== false) {
				find = common.replaceNewLine(find, this.project.files[file].lineBreak);
			}
		}

		// not support case insensitive
/*
		if (thisAddress?.rowObj?.length) {
			// the newest Address object directly point to row's memory address
			console.log("Found rowObject", thisAddress?.rowObj);
			
		} else 
*/
		if (typeof row == 'number') {
			var thisRow = this.getData(file)[row];
			if (!thisRow) continue;
			if (options.filterTagMode == "blacklist") {
				if (this.hasTags(options.filterTag, row, file)) continue;
			} else if (options.filterTagMode == "whitelist") {
				if (!this.hasTags(options.filterTag, row, file)) continue;
			}


			if (options.lineByLine) {
				var currentText = thisRow[columns] || "";
				var currentLines = currentText.replaceAll("\r", "").split("\n");
				if (options.overwrite == false) {
					if (Boolean(currentLines[thisAddress.line]) == true) continue;
				}
				currentLines[thisAddress.line] = value
				thisRow[columns] = currentLines.join("\n");

			} else {
				// plain row by row
				if (options.overwrite == false) {
					if (Boolean(thisRow[columns]) == true) continue;
				}
				thisRow[columns] = value;
			}

			result.files[file] = result.files[file] || [];
			result.files[file].push({
				'fullString':thisRow[columns],
				'row':row,
				'col':columns,
				'type':'cell',
				'lineIndex':null
			});
			result.count++;	
		}
	}

	return result;	
}

/**
 * Find a key string, then insert a text into the adjacent cell
 * @param {String} find - String to find.
 * @param {String} values - Text to put into
 * @param {Number} columns - Column ID to put the values into
 * @param {Object} options 
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the existing text on the destination cell or not
 * @param {String[]} [options.files=this.getAllFiles()] - Selected files
 * @param {Boolean} [options.keyColumn=0] - Key column id
 * @param {Boolean} [options.insensitive=false] - Whether the test is using insensitive case or not
 */
 Trans.prototype.findAndInsert = function(find, values, columns, options) {
	// find key "find", put "values" to coloumn with index "columns" 
	// to all files inside trans.project.files	
	//console.log("trans.findAndInsert", arguments);
	columns = columns||1;
	options = options||{};
	options.overwrite 		= options.overwrite||false;
	options.files 			= options.files||[];
	options.keyColumn		= options.keyColumn || this.keyColumn || 0;
	options.insensitive		= options.insensitive || false;

	
	if (typeof options.ignoreNewLine == "undefined") options.ignoreNewLine = true;


	//console.log("incoming parameters : ");
	//console.log(arguments);
	
	if (options.files.length == 0) { // that means ALL!
		options.files = this.getAllFiles();
	}
	
	var result = {
		keyword	:find,
		count	:0,
		executionTime:0,
		files	:{}
	};

	for (let i=0; i<options.files.length; i++) {
		let file = options.files[i];
		if (options.ignoreNewLine) {
			if (Boolean(this.project.files[file].lineBreak) !== false) {
				find = common.replaceNewLine(find, this.project.files[file].lineBreak);
			}
		}
		var row
		if (options.insensitive) {
			row = this.getRowIdByTextInsensitive(find, file)
		} else {
			row = this.findIdByIndex(find, file);
		}

		if (typeof row == 'number') {
			var thisRow = this.getData(file)[row];
			if (!thisRow) continue;
			if (options.filterTagMode == "blacklist") {
				if (this.hasTags(options.filterTag, row, file)) continue;
			} else if (options.filterTagMode == "whitelist") {
				if (!this.hasTags(options.filterTag, row, file)) continue;
			}

			if (options.overwrite == false) {
				if (Boolean(thisRow[columns]) == true) continue;
			}
			thisRow[columns] = values;

			result.files[file] = result.files[file] || [];
			result.files[file].push({
				'fullString':thisRow[columns],
				'row':row,
				'col':columns,
				'type':'cell',
				'lineIndex':null
			});
			result.count++;	
		}
	}

	return result;
}

/**
 * Find a key string, then insert a text into the adjacent cell using line-by-line algorithm
 * @param {String} find - String to find.
 * @param {String} values - Text to put into
 * @param {Number} columns - Column ID to put the values into
 * @param {Object} options 
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the existing text on the destination cell or not
 * @param {String[]} [options.files=this.getAllFiles()] - Selected files
 * @param {Boolean} [options.keyColumn=0] - Key column id
 * @param {Boolean} [options.insensitive=false] - Whether the test is using insensitive case or not
 * @param {String} [options.newLine=\n] - New line character
 * @param {Boolean} [options.stripCarriageReturn=\n] - Whether to strip charriage return character (\r) or not
 */
 Trans.prototype.findAndInsertLine = function(find, values, columns, options) {
	// find key "find", put "values" to coloumn with index "columns" 
	// to all files inside trans.project.files	
	//console.log("findAndInsertLine:", arguments);
	columns 			= columns||1;
	options 			= options||{};
	options.overwrite 	= options.overwrite||false;
	options.files 		= options.files||[];
	options.keyColumn 	= options.keyColumn||0;
	options.newLine 	= options.newLine||undefined;
	options.stripCarriageReturn = options.stripCarriageReturn||false;
	
	if (options.stripCarriageReturn) {
		find = common.stripCarriageReturn(find);
	}
	
	if (options.files.length == 0) { // that means ALL!
		if (typeof trans.allFiles != 'undefined') {
			options.files = trans.allFiles;
		} else {
			for (var file in trans.project.files) {
				options.files.push(file);
			}
			trans.allFiles = options.files;
		}
	}
	for (var i=0; i<options.files.length; i++) {
		file = options.files[i];
		var thisData = trans.project.files[file].data;
		var thisNewLine = options.newLine||trans.project.files[file].lineBreak||"\n";
		for (var row=0; row<thisData.length; row++) {
			let keySegment
			if (!thisData[row][options.keyColumn]) continue;
			
			if (options.filterTagMode == "blacklist") {
				if (this.hasTags(options.filterTag, row, file)) continue;
			} else if (options.filterTagMode == "whitelist") {
				if (!this.hasTags(options.filterTag, row, file)) continue;
			}

			thisData[row][options.keyColumn] = thisData[row][options.keyColumn]||"";
			if (options.stripCarriageReturn) {
				keySegment = thisData[row][options.keyColumn].replaceAll("\r", "").split("\n");
				/*
				var keySegment = thisData[row][options.keyColumn].split("\n").map(function(input) {
										return common.stripCarriageReturn(input);
									});
				*/
			} else {	
				keySegment = thisData[row][options.keyColumn].split("\n");
			}
			if (keySegment.includes(find) == false) continue;				
			thisData[row][columns] 	= thisData[row][columns]||"";
			var targetSegment 		= thisData[row][columns].replaceAll("\r", "").split("\n");
			/*
			var targetSegment =	thisData[row][columns].split("\n").map(function(input) {
									return common.stripCarriageReturn(input);
								});
			*/

			targetSegment 			= common.searchReplaceArray(keySegment, targetSegment, find, values, {overwrite:options.overwrite});
			thisData[row][columns] 	= targetSegment.join(thisNewLine);
		}

	}
}

/**
 * find key "find", put "values" to coloumn with index "columns" 
 * to all files inside trans.project.files	
 * @param {String} find - String to find.
 * @param {String} values - Text to put into
 * @param {Number} columns - Column ID to put the values into
 * @param {Object} options 
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the existing text on the destination cell or not
 * @param {String[]} [options.files=this.getAllFiles()] - Selected files
 * @param {Boolean} [options.ignoreNewLine] - Whether to ignore new line characters or not
 */
 Trans.prototype.findAndInsertByContext = function(find, values, columns, options) {
	//console.log("trans.findAndInsertByContext:", arguments);
	columns 			= columns||1;
	options 			= options||{};
	options.overwrite 	= options.overwrite||false;
	options.files 		= options.files||[];
	options.ignoreNewLine = options.ignoreNewLine||false;
	
	if (options.files.length == 0) { // that means ALL!
		options.files = this.getAllFiles();
	}

	for (let i=0; i<options.files.length; i++) {
		let file = options.files[i];
		var thisObj = this.getObjectById(file);
		if (Boolean(thisObj) == false) continue;
		if (Boolean(thisObj.context) == false) continue;

		for (var row=0; row<thisObj.context.length; row++) {
			var contextByRow = thisObj.context[row];
			if (Array.isArray(contextByRow) == false) continue;
			if (contextByRow.length < 1) continue;
			if (Array.isArray(thisObj.data[row]) == false) continue;
			for (var contextId=0; contextId<contextByRow.length; contextId++) {
				if (find !== contextByRow[contextId]) continue;
				// match found! assign value
				if (options.overwrite == false) {
					if (Boolean(thisObj.data[row][columns]) == true) continue;
				}				
				thisObj.data[row][columns] = values;
			}
		}
	}
}

/**
 * Translate text with line-by-line algorithm
 * @param {String} text - multilined text
 * @param {Object} translationPair - Key pair translation object
 */
 Trans.prototype.translateTextByLine = function(text, translationPair, options) {
	/*
		translating text line by line by translationPair
		text : multilined text
		translationPair: object : {"key":"translation"}
		
		output: translated text
		
		todo : make line break type match original text
	*/
	//console.log("trans.translateTextByLine", arguments);
	if (typeof text !== 'string') return text;
	if (text.length < 1) return text;
	
	// Up to ver 3.8.21 blank text will returns original text
	//var translated = text;
	var translated = "";
	var translatedLine = [];
	var lines = text.replace(/(\r\n)/gm, "\n").split("\n");
	//var lines = text.split("\n");
	//console.log("lines are:", lines);
	//console.log("%c-With translation pair:", "color:#0f0;", JSON.stringify(translationPair, undefined, 2))
	for (var i=0; i<lines.length; i++) {
		var line = lines[i];
		//console.log("%c--Comparing:", "color:#0f0;", JSON.stringify(line))
		if (translationPair[line]) {
			translatedLine.push(translationPair[line]);
			//console.log("%c--found", "color:#0f0;")
			continue;
		}
		//console.log("%c--not found", "color:#0f0;")

		// Up to ver 3.8.21 blank text will returns original text
		//translatedLine.push(line);
		translatedLine.push("");
	}
	//console.log("translatedLine:", translatedLine);
	translated = translatedLine.join("\n");
	
	//console.log("Result of trans.translateTextByLine", translated);
	return translated;
	
}

/**
 * translate from array obj
 * @param {*} obj 
 * @param {Number} columns 
 * @param {Object} options 
 * @param {Number|'auto'} options.sourceColumn 
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the existing text on the destination cell or not
 * @param {Number} [options.sourceKeyColumn=0] - The source key column
 * @param {String[]} [options.files=this.getAllFiles()] - Selected files
 * @param {Boolean} [options.keyColumn=0] - Key column id
 * @param {String} [options.newLine=\n] - New line character
 * @param {Boolean} [options.stripCarriageReturn=\n] - Whether to strip charriage return character (\r) or not
 */
 Trans.prototype.translateFromArray = function(obj, columns, options) {
	// translate from array obj
	console.log("insert trans.translateFromArray", arguments);

	columns = columns||1;
	options = options||{};
	options.sourceColumn = options.sourceColumn||"auto";
	options.overwrite = options.overwrite||false;
	options.files = options.files||[];
	options.sourceKeyColumn = options.sourceKeyColumn||0;
	options.keyColumn = options.keyColumn||0;
	options.newLine = options.newLine||undefined;
	options.stripCarriageReturn = options.stripCarriageReturn||false;
	options.ignoreNewLine = true; // let's set to true;
	//options.translationMethod = options.translationMethod||false;
	
	obj = obj||[];

	if (obj.length == 0) return false;
	

	if (!options.indexes) {
		if (options.files?.length == 0) {
			options.files = this.getAllFiles();
		}
		options.indexes = this.buildIndexes(options.files, false);
	}

	for (var rowId=0; rowId<obj.length; rowId++) {
		var translation;
		var row = obj[rowId];
		if (Boolean(row[options.sourceKeyColumn]) == false) continue;
		
		var keyString = row[options.sourceKeyColumn];
		if (options.sourceColumn == 'auto') {
			translation = trans.getTranslationFromRow(row, options.sourceKeyColumn);
		} else {
			translation = row[options.sourceColumn];
		}
		//trans.findAndInsert = function(find, values, columns, options)
		//trans.findAndInsert(keyString, translation, columns, options);
		trans.findAndInsertWithIndexes(	
			keyString, 
			translation, 
			columns, 
			options.indexes,
			{
				overwrite	:options.overwrite,
				files		:options.files
			});
	}
}

/**
 * Abort translation process
 */
 Trans.prototype.abortTranslation = function() {
	trans.translator = trans.translator||[];
	if (typeof trans.translationTimer !== 'undefined') {
		clearTimeout(trans.translationTimer);
	}

	for (var i=0; i<trans.translator.length; i++) {
		let translator = trans.translator[i];
		var thisTranslator = trans.getTranslatorEngine(translator)
		if (!thisTranslator) continue;

		if (typeof thisTranslator.abort == "function") {
			try {
				thisTranslator.abort();
			} catch (e) {
				ui.log("Failed to abort with message", e.toString());
			}
		}
		thisTranslator.job = thisTranslator.job||{};
		thisTranslator.job.wordcache = {};
		thisTranslator.job.batch = [];
	}
	ui.hideLoading();
	trans.refreshGrid();
	trans.evalTranslationProgress();
}

/**
 * Translate string by translation pair
 * @param {String} str - String to translate
 * @param {Object} translationPair - Key value based translation pair object
 * @param {Boolean} caseInSensitive - Whether or not the translation is in case insensitive mode
 * @returns {String} Translated result
 */
 Trans.prototype.translateStringByPair = function(str, translationPair, caseInSensitive) {
	caseInSensitive = caseInSensitive||false;
	if (typeof str !== 'string') return str;
	if (typeof translationPair !== 'object') return str;
	for (var key in translationPair) {
		str = str.replaces(key, translationPair[key], caseInSensitive);
	}	
	return str;
}

/**
 * Search a translation from given trans data
 * @param {String|String[]} text - Original text to translate 
 * @param {String|String[]} [fileFilter] - Destination file ID to search into. If blank then the method will search all file object 
 * @param {Trans} [transData=this] - Trans Data
 * @since 6.1.14
 * @returns {String|undefined} - translation result
 */
Trans.prototype.translateFromTrans = function(text, fileFilter = [], transData = this) {
	if (!text) return "";
	if (typeof fileFilter == "string") fileFilter = [fileFilter];

	const files = transData?.project?.files || {};
	if (!fileFilter?.length) {
		fileFilter = this.getAllFiles(files);
	}
	
	const getInstance = (text) => {
		for (let i in fileFilter) {
			let fileId = fileFilter[i]
			let index = this.getIndexByKey(fileId, text);
			if (typeof index == "undefined") continue;
			let data = this.getData(fileId);
			if (empty(data[index])) return "";
			let translation = this.getTranslationFromRow(data[index])
			if (translation) return translation;
		}
	}

	if (Array.isArray(text)) {
		let result = []
		for (let i in text) {
			result[i] = getInstance(text[i]) || ""
		}
		return result;
	} else {
		return getInstance(text);
	}
}

/**
 * Search a string from reference, will return blank if not found.
 * @param {String} string - String to search for
 * @param {String} [file=Common Reference] - File to be used as reference
 * @returns {String} Translated text, blank if no reference found.
 */
 Trans.prototype.getReference = function(string, file = "Common Reference") {
	if (!string) return "";
	var index = this.getIndexByKey(file, string);
	if (typeof index == "undefined") return "";
	var data = this.getData(file);
	if (empty(data[index])) return "";

	return this.getTranslationFromRow(data[index]) || "";
}

/**
 * Get translation from common reference. The behavior is like translation procedure.
 * Will return original text if no reference found
 * @param {String} input - String to be translated
 * @param {Boolean} [caseInSensitive=false] - Whether to perform case insensitive search or not
 * @param {String} [referenceName="Common Reference"] - The name of the reference file object
 * @returns {String} translated string
 */
 Trans.prototype.translateByReference = function(input, caseInSensitive, referenceName="Common Reference") {
	caseInSensitive = caseInSensitive||false;
	//data = JSON.parse(JSON.stringify(input));
	//console.log("Reference", referenceName);
	if (!trans.project.files[referenceName]) return input;

	trans.project.files[referenceName]["cacheResetOnChange"] = trans.project.files[referenceName]["cacheResetOnChange"]||{};
	var transPair;
	if (Boolean(trans.project.files[referenceName]["cacheResetOnChange"]["transPair"])!== false) {
		//console.log("load translation pair from cache");
		transPair = trans.project.files[referenceName]["cacheResetOnChange"]["transPair"];
	} else {
		transPair = trans.generateTranslationPair(trans.project.files[referenceName].data);
		trans.project.files[referenceName]["cacheResetOnChange"]["transPair"] = transPair;
	}
	//console.log("Translate by reference pair :", transPair);
	var output
	if (typeof input == 'string') {
		output = trans.translateStringByPair(input, transPair, caseInSensitive);
		return output;
	} else if (Array.isArray(input)){
		output = [];
		for (var i=0; i<input.length; i++) {
			output[i] = trans.translateStringByPair(input[i], transPair, caseInSensitive);
		}
		return output;
		
	}
	return input;
}

/**
 * Check whether a row has translation or not
 * @param {String[]} row - Single dimensional array representing rows
 * @param {Number} [keyColumn=0] 
 * @returns {Boolean} True if has translation
 */
 Trans.prototype.rowHasTranslation = function(row, keyColumn) {
	keyColumn = this.keyColumn || 0;
	if (empty(row)) return false;
	for (var i=0; i<row.length; i++) {
		if (i == keyColumn) continue;
		if (row[i]) return true;
	}
	return false;
}

/**
 * Execute batch translation and automatically detect translation mode
 * @param {TranslatorEngine} translator - Selected Translator engine object
 * @param {Object} options 
 * @param {Function} [options.onFinished] - Function to run when the process completed
 * @param {Number} [options.keyColumn=0] - Key column index of the table
 * @param {Boolean} [options.translateOther] - Whether to translate unselected files or not
 * @param {Boolean} [options.saveOnEachBatch] - Whether to save project for each batch
 * @param {String[]} [options.filterTag] - Tags filter
 * @param {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite or not if the destination cell is not empty
 * @param {Boolean} [options.ignoreTranslated=false] - Whether to skip processing or not if the row already has translation
 */
 Trans.prototype.translateAll = function(translator, options) {
	var activeTranslator = this.getTranslatorEngine(translator);
	this.buildIndex("Common Reference", true);
	if (activeTranslator.mode == "rowByRow") {
		trans.translateAllByRows(translator, options);
	} else {
		trans.translateAllByLines(translator, options);
	}
}

/**
 * Execute batch translation with row by row mode
 * @param {TranslatorEngine} translator - Selected Translator engine object
 * @param {Object} options 
 * @param {Function} [options.onFinished] - Function to run when the process completed
 * @param {Number} [options.keyColumn=0] - Key column index of the table
 * @param {Boolean} [options.translateOther] - Whether to translate unselected files or not
 * @param {Boolean} [options.saveOnEachBatch] - Whether to save project for each batch
 * @param {String[]} [options.filterTag] - Tags filter
 * @param {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite or not if the destination cell is not empty
 * @param {Boolean} [options.ignoreTranslated=false] - Whether to skip processing or not if the row already has translation
 */
 Trans.prototype.translateAllByRows = async function(translator, options) {
	// TRANSLATE ALL
	console.log("Running trans.translateAll", options);
	ui.loadingProgress(0, "Running trans.translateAll");
	var thisTranslator = this.getTranslatorEngine(translator);
	if (typeof trans.project == 'undefined') return trans.alert(t("Unable to process, project not found"));
	if (typeof trans.project.files == 'undefined') return trans.alert(t("Unable to process, data not found"));
	if (typeof thisTranslator == 'undefined')  return trans.alert(t("Translation engine not found"));
	if (thisTranslator.isDisabled)  return trans.alert(t("Translation engine ")+translator+t(" is disabled!"));
	
	if (!trans.getSelectedId()) {
		trans.selectFile($(".fileList .data-selector").eq(0));
	}
	
	
	options = options||{};
	options.onFinished 		= options.onFinished||function() {};
	options.keyColumn 		= options.keyColumn||0;
	options.translateOther 	= options.translateOther|| false;
	options.ignoreTranslated= options.ignoreTranslated || false;
	options.overwrite		= options.overwrite || false;
	options.saveOnEachBatch	= options.saveOnEachBatch || false;
	
	// COLLECTING DATA
	thisTranslator.job = thisTranslator.job||{};
	thisTranslator.job.wordcache = {};
	thisTranslator.job.batch = [];
	thisTranslator.batchDelay = thisTranslator.batchDelay||trans.config.batchDelay;

	// CALCULATING max request length
	var currentMaxLength = trans.config.maxRequestLength;
	if (thisTranslator.maxRequestLength < currentMaxLength) currentMaxLength = thisTranslator.maxRequestLength;
	
	// SHOW loading bar
	ui.showLoading({
		buttons:[{
			text:"Abort",
			onClick: function(e) {
				var sure = confirm(t("Are you sure want to abort this process?"));
				if (sure) trans.abortTranslation();
			}
		},
		{
			text:"Pause",
			onClick: function(e) {
				alert(t("Process paused!\nPress OK to continue!"));

			}
		}
		]
	});
	ui.log("Start batch translation in Row by Row mode, with options:", options);
	ui.log(`Translator is: ${translator}`);
	console.log("Current maximum request length : "+currentMaxLength);
	console.log("Start collecting data!");
	ui.loadingProgress(0, "Start collecting data!");	



	var currentBatchID 			= 0;
	var currentRequestLength 	= 0;

	// to store key and source column pair when source column !=0;
	var keyIndex				= {};
	
	// collecting selected row

	var selectedFiles = trans.getCheckedFiles();
	ui.loadingProgress(undefined, t("Selected files :")+selectedFiles.join(", "));	

	// none selected means all
	if (selectedFiles.length == 0) selectedFiles = this.getAllFiles();
	
	for (var thisIndex in selectedFiles) {
		var file = selectedFiles[thisIndex];

		ui.loadingProgress(undefined, t("Collecting data from :")+file);	
		
		try {
			var currentData = trans.project.files[file].data;
		} catch(e) {
			console.warn("Can not access", file, "in trans.project.files" );
			continue;
		}
		
		if (typeof thisTranslator.job.batch[currentBatchID] == 'undefined') thisTranslator.job.batch[currentBatchID] = [];
		
		for (var i=0; i< currentData.length; i++) {
			// skip if col[0] is empty, regardless keyColum, because we will use that latter
			if (!currentData[i][0]) continue; 
			var currentSentence = currentData[i][options.keyColumn];
			var escapedSentence = str_ireplace($DV.config.lineSeparator, thisTranslator.lineSubstitute, thisTranslator.escapeCharacter(currentSentence));

			// skiping when empty
			if (!currentSentence) continue;
			
			
			// skip according to tags
			if (options.filterTagMode == "blacklist") {
				if (this.hasTags(options.filterTag, i, file)) continue;
			} else if (options.filterTagMode == "whitelist") {
				if (!this.hasTags(options.filterTag, i, file)) continue;
			}
			
			// skip line that already translated
			if (options.ignoreTranslated) {
				if (this.rowHasTranslation(currentData[i], options.keyColumn)) continue;
			}

			// skip line if cell is not empty & overwrite option is false
			if (options.overwrite == false) {
				if (currentData[i][thisTranslator.targetColumn]) {
					console.log("Ignored because overwite options", options.overwrite, currentData[i][thisTranslator.targetColumn]);
					continue;
				}
			}

			if (currentSentence.trim().length == 0) continue;
			
			// assign keyIndex pair
			keyIndex[currentSentence] = currentData[i][0];

			if (escapedSentence.length > currentMaxLength) {
				console.log('current sentence is bigger than maxRequestLength!');
				currentBatchID++;
				thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
				thisTranslator.job.batch[currentBatchID].push(currentSentence);
				currentBatchID++;
				currentRequestLength = 0;
				continue;
			}
			//if (currentSentence.length+currentRequestLength > currentMaxLength) {
			if (escapedSentence.length+currentRequestLength > currentMaxLength) {
				currentBatchID++;
				thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
				thisTranslator.job.batch[currentBatchID].push(currentSentence);
				currentRequestLength = 0;
				continue;
		
			}
			

			// make each line unique
			if (typeof thisTranslator.job.wordcache[currentSentence] !== 'undefined') {
				continue;
			} else {
				thisTranslator.job.wordcache[currentSentence] = true;
			}

			thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
			thisTranslator.job.batch[currentBatchID].push(currentSentence);
			//currentRequestLength += currentSentence.length;
			currentRequestLength += escapedSentence.length;
		}
	}

	await ui.log("Generating indexes");
	var tempIndexes = this.buildIndexes(selectedFiles);
	
	thisTranslator.job.batchLength = thisTranslator.job.batch.length;
	console.log("current batch:", thisTranslator.job.batch);
	ui.loadingProgress(0, "Data collection is done!");	
	
	console.log("Data collection is done!");
	console.log("We have "+thisTranslator.job.batch.length+" batch totals!");
	console.log("==========================================");
	console.log("Begin translating using "+translator+"!");
	
	
	var processPart = async function() {
		if (typeof thisTranslator.job.batch == 'undefined') return "batch job undefined, quiting!";
		if (thisTranslator.job.batch.length < 1) {
			console.log("Batch job is finished");
			trans.refreshGrid();
			trans.evalTranslationProgress();
			if (typeof options.onFinished == 'function') {
				options.onFinished.call(this);
			}
			ui.loadingProgress(100, t("Translation finished"));
			//ui.hideLoading();
			ui.loadingClearButton();
			ui.loadingEnd();
			trans.onBatchTranslationDone(options);
			trans.translationTimer = undefined;
			return "batch job is finished!";
		}
		console.log("running processPart");
		
		var currentData = thisTranslator.job.batch.pop();
		
		console.log("current data : ");
		console.log(currentData);
		
		var preTransData;
		if (thisTranslator.skipReferencePair) {
			preTransData = currentData;
		} else {
			preTransData = trans.translateByReference(currentData);
		}
			
		thisTranslator.translate(preTransData, {
			mode: "rowByRow",
			onAfterLoading:async function(result) {
				console.log(result);
				if (typeof result.translation !== 'undefined') {
					console.log("applying translation to table !");
					console.log("calculating progress");
					var percent = thisTranslator.job.batch.length/thisTranslator.job.batchLength*100;
					percent = 100-percent;
					ui.loadingProgress(percent, t("applying translation to table!"));	
					
					var targetFiles = selectedFiles;
					if (options.translateOther) targetFiles = trans.getAllFiles();
					trans.trigger("batchTranslationResult", {original:currentData, translation:result.translation, translator:thisTranslator})
					for (var index in result.translation) {
						//trans.findAndInsert(keyIndex[currentData[index]],
						trans.findAndInsertWithIndexes(
							keyIndex[currentData[index]],
							result.translation[index], 
							thisTranslator.targetColumn, 
							tempIndexes,
							{
								filterTag 		: options.filterTag || [],
								filterTagMode	: options.filterTagMode,
								keyColumn		: options.keyColumn,
								overwrite		: options.overwrite,
								files			: targetFiles
							});
					}
					if (options.saveOnEachBatch) {
						ui.log("Saving your project");
						await trans.save();
					}
					ui.loadingProgress(undefined, (thisTranslator.job.batchLength-thisTranslator.job.batch.length)+"/"+thisTranslator.job.batchLength+" batch done, waiting "+thisTranslator.batchDelay+" ms...");	
					trans.translationTimer = setTimeout(function(){ processPart(); }, thisTranslator.batchDelay);
					
				}
				//trans.refreshGrid();
				//trans.grid.render();
			},
			onError:function(evt, type, errorType) {
					console.log("ERROR on transling data");
					var percent = thisTranslator.job.batch.length/thisTranslator.job.batchLength*100;
					percent = 100-percent;
					ui.loadingProgress(percent, 
						t("Error when translating data!")+"\r\n"+
						t("\tHTTP Status : ")+evt.status+"\r\n"+
						t("\tError Type : ")+errorType+"\r\n"+
						t("You are probably temporarily banned by your translation service!\r\nPlease use online translation service in moderation\r\nThis usualy fixed by itself within a day or two.\r\n"));	
					ui.loadingProgress(undefined, (thisTranslator.job.batchLength-thisTranslator.job.batch.length)+"/"+thisTranslator.job.batchLength+t(" batch done, ")+t("waiting ")+thisTranslator.batchDelay+t(" ms..."));	
					trans.translationTimer = setTimeout(function(){ processPart(); }, thisTranslator.batchDelay);
				
			}
		});
	}
	processPart();
	
}

/**
 * Execute batch translation with line-by-line mode
 * @param {TranslatorEngine} translator - Selected Translator engine object
 * @param {Object} options 
 * @param {Function} [options.onFinished] - Function to run when the process completed
 * @param {Number} [options.keyColumn=0] - Key column index of the table
 * @param {Boolean} [options.translateOther] - Whether to translate unselected files or not
 * @param {Boolean} [options.saveOnEachBatch] - Whether to save project for each batch
 * @param {String[]} [options.filterTag] - Tags filter
 * @param {'blacklist'|'whitelist'} [options.filterTagMode] - Filter mode
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite or not if the destination cell is not empty
 * @param {Boolean} [options.ignoreTranslated=false] - Whether to skip processing or not if the row already has translation
 */
 Trans.prototype.translateAllByLines = async function(translator, options) {
	// TRANSLATE ALL
	console.log("Running trans.translateAllByLines", arguments);
	ui.loadingProgress(0, t("Running trans.translateAll"));	
	var thisTranslator = this.getTranslatorEngine(translator);	
	if (typeof trans.project == 'undefined') return trans.alert(t("Unable to process, project not found"));
	if (typeof trans.project.files == 'undefined') return trans.alert(t("Unable to process, data not found"));
	if (typeof thisTranslator == 'undefined')  return trans.alert(t("Translation engine not found"));
	if (thisTranslator.isDisabled)  return trans.alert(t("Translation engine ")+translator+t(" is disabled!"));
	
	if (!trans.getSelectedId()) {
		trans.selectFile($(".fileList .data-selector").eq(0));
	}
	
	
	options 				= options||{};
	options.onFinished 		= options.onFinished||function() {};
	options.keyColumn 		= options.keyColumn||0;
	options.translateOther 	= options.translateOther|| false;
	options.ignoreTranslated= options.ignoreTranslated || false;
	options.overwrite		= options.overwrite || false;
	options.saveOnEachBatch	= options.saveOnEachBatch || false;

	var fetchMode = "both"
	if (options.ignoreTranslated) {
		fetchMode = "untranslated"
	}
	
	// COLLECTING DATA
	thisTranslator.job = thisTranslator.job||{};
	thisTranslator.job.wordcache = {};
	thisTranslator.job.batch = [];
	thisTranslator.batchDelay = thisTranslator.batchDelay||trans.config.batchDelay;
	
	// CALCULATING max request length
	var currentMaxLength = trans.config.maxRequestLength;
	if (thisTranslator.maxRequestLength < currentMaxLength) currentMaxLength = thisTranslator.maxRequestLength;
	
	// SHOW loading bar
	ui.showLoading({
		buttons:[{
			text:"Abort",
			onClick: function(e) {
				var sure = confirm(t("Are you sure want to abort this process?"));
				if (sure) trans.abortTranslation();
			}
		},
		{
			text:"Pause",
			onClick: function(e) {
				alert(t("Process paused!\nPress OK to continue!"));

			}
		}
		]
	});
	ui.log("Start batch translation in Line by Line mode, with options:", options);
	ui.log(`Translator is: ${translator}`);
	console.log("Current maximum request length : "+currentMaxLength);
	console.log("Start collecting data!");
	ui.loadingProgress(0, t("Start collecting data!"));	
	
	var currentBatchID = 0;
	var currentRequestLength = 0;
	

	// collecting selected row
	var selectedFiles = trans.getCheckedFiles();
	ui.loadingProgress(undefined, t("Selected files :")+selectedFiles.join(", "));		
	
	var transTableInfo = trans.generateTranslationTableLine(trans.project, {
			files				: selectedFiles, 
			mode				: "lineByLine", 
			fetch				: fetchMode,
			keyColumn			: options.keyColumn,
			filterLanguage		: this.getSl(),
			filterTag 			: options.filterTag || [],
			filterTagMode		: options.filterTagMode,
			ignoreTranslated 	: options.ignoreTranslated,
			targetColumn		: thisTranslator.targetColumn,
			overwrite			: options.overwrite,
			ignoreWhitespace	: thisTranslator.ignoreWhitespace,
            collectAddress      : true
		});
    var transTable = transTableInfo.pairs;
	console.log("Fetch mode : ", fetchMode);
	console.log("Translatable : ", transTable);

	currentBatchID = 0;
	currentRequestLength = 0;
	
	for (var translateKey in transTable) {
		thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
		
		
		var currentSentence = translateKey;
		var escapedSentence = str_ireplace($DV.config.lineSeparator, thisTranslator.lineSubstitute, thisTranslator.escapeCharacter(currentSentence));

		// skiping when empty
		if (Boolean(currentSentence) == false) continue;
		if (typeof currentSentence !== 'string') continue;
		if (currentSentence.trim().length == 0) continue;

		if (escapedSentence.length > currentMaxLength) {
			console.log('current sentence is bigger than maxRequestLength!');
			currentBatchID++;
			thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
			thisTranslator.job.batch[currentBatchID].push(currentSentence);
			currentBatchID++;
			currentRequestLength = 0;
			continue;
		}

		if (escapedSentence.length+currentRequestLength > currentMaxLength) {
			currentBatchID++;
			thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
			thisTranslator.job.batch[currentBatchID].push(currentSentence);
			currentRequestLength = 0;
			continue;
	
		}
		
		thisTranslator.job.batch[currentBatchID] = thisTranslator.job.batch[currentBatchID]||[];
		thisTranslator.job.batch[currentBatchID].push(currentSentence);
		//currentRequestLength += currentSentence.length;
		currentRequestLength += escapedSentence.length;
	}
	
	// cleaning up transTable for RAM friendly
	transTable = undefined;


	thisTranslator.job.batchLength = thisTranslator.job.batch.length;
	ui.loadingProgress(0, t("Data collection is done!"));	
	
	console.log("Data collection is done!");
	console.log("We have "+thisTranslator.job.batch.length+" batch totals!");
	console.log("==========================================");
	console.log("Begin translating using "+translator+"!");
	
	await ui.log("Generating indexes");
	var tempIndexes = this.buildIndexes(selectedFiles, true);

	console.log("indexes:", tempIndexes);

	var processPart = async function() {
		if (typeof thisTranslator.job.batch == 'undefined') return "batch job undefined, quiting!";
		if (thisTranslator.job.batch.length < 1) {
			console.log("Batch job is finished");
			// filling skipped translation

			var selectedObj = [];
			if (options.translateOther == false) selectedObj = trans.getCheckedFiles();
			trans.fillEmptyLine(selectedObj, [], thisTranslator.targetColumn, options.keyColumn, {
				lineFilter : function(str) {
					return !common.isInLanguage(str, trans.getSl());
				},
				fromKeyOnly		: options.ignoreTranslated,
				filterTag 		: options.filterTag || [],
				filterTagMode	: options.filterTagMode,
				overwrite		: options.overwrite
			});
			
			
			trans.refreshGrid();
			trans.evalTranslationProgress();
			if (typeof options.onFinished == 'function') {
				options.onFinished.call(this);
			}
			ui.loadingProgress(100, t("Translation finished"));
			//ui.hideLoading();
			ui.loadingClearButton();
			ui.loadingEnd();
			trans.onBatchTranslationDone(options);
			trans.translationTimer = undefined;
			return "batch job is finished!";
		}
		
		var translatePart = async function() {
			console.log("running processPart");
			var selectedObj = [];
			if (options.translateOther == false) selectedObj = trans.getCheckedFiles();
			trans.fillEmptyLine(selectedObj, [], thisTranslator.targetColumn, options.keyColumn, {
				lineFilter : function(str) {
					return !common.isInLanguage(str, trans.getSl());
				},
				fromKeyOnly		: options.ignoreTranslated,
				filterTag 		: options.filterTag || [],
				filterTagMode	: options.filterTagMode,
				overwrite		: options.overwrite
			});
	
			var currentData = thisTranslator.job.batch.pop();
			console.log("current data : ");
			console.log(currentData);

			var preTransData;
			if (thisTranslator.skipReferencePair) {
				preTransData = currentData;
			} else {
				preTransData = trans.translateByReference(currentData);
			}
			
			thisTranslator.translate(preTransData, {
				onAfterLoading:async function(result) {
					console.log("onAfterLoading translation result:", result);
					if (typeof result.translation !== 'undefined') {
						console.log("applying translation to table !");
						//console.log("calculating progress");
						var percent = thisTranslator.job.batch.length/thisTranslator.job.batchLength*100;
						percent = 100-percent;
						ui.loadingProgress(percent, t("applying translation to table!"));	
						
						trans.trigger("batchTranslationResult", {original:currentData, translation:result.translation, translator:thisTranslator})

						for (var index in result.translation) {
							trans.findAndInsertWithIndexes(
								currentData[index], 
								result.translation[index],
								thisTranslator.targetColumn,
								//tempIndexes,
								transTableInfo.addresses,
								{
									files				: selectedObj,
									keyColumn			: options.keyColumn,
									stripCarriageReturn	: true,
									filterTag 			: options.filterTag || [],
									filterTagMode		: options.filterTagMode,
									overwrite			: options.overwrite,
									lineByLine			: true
								});
						}
						
						if (options.saveOnEachBatch) {
							ui.log("Saving your project");
							await trans.save();
						}
	
						//ui.loadingProgress(undefined, (thisTranslator.job.batchLength-thisTranslator.job.batch.length)+"/"+thisTranslator.job.batchLength+" batch done, waiting "+thisTranslator.batchDelay+" ms...");	
						ui.loadingProgress(undefined, (thisTranslator.job.batchLength-thisTranslator.job.batch.length)+"/"+thisTranslator.job.batchLength+" batch done!");	
						processPart();
						
					}
				},
				onError:function(evt, type, errorType) {
						console.log("ERROR on transling data");
						var percent = thisTranslator.job.batch.length/thisTranslator.job.batchLength*100;
						percent = 100-percent;
						ui.loadingProgress(percent, 
							t("Error when translating data!")+"\r\n"+
							t("\tHTTP Status : ")+evt.status+"\r\n"+
							t("\tError Type : ")+errorType+"\r\n"+
							t("You are probably temporarily banned by your translation service!\r\nPlease use online translation service in moderation\r\nThis usualy fixed by itself within a day or two.\r\n"));	
						ui.loadingProgress(undefined, (thisTranslator.job.batchLength-thisTranslator.job.batch.length)+"/"+thisTranslator.job.batchLength+" batch done!");	
						processPart();
					
				}
			});
		}
		
		if (thisTranslator.job.batchLength == thisTranslator.job.batch.length) {
			translatePart();
		} else {
			ui.loadingProgress(undefined, "Waiting "+thisTranslator.batchDelay+" ms...");	
			trans.translationTimer = setTimeout(function(){ translatePart(); }, thisTranslator.batchDelay);
		}
		
	} 
	processPart();

}

/**
 * Function to run when translation is completed
 * @param {Object} options The same options with trans.translateAll function
 */
 Trans.prototype.onBatchTranslationDone = function(options) {
	if (options.playSoundOnComplete) {
		var myAudioElement = new Audio('/www/audio/done.mp3');
		myAudioElement.addEventListener("canplay", event => {
			/* the audio is now playable; play it if permissions allow */
			myAudioElement.play();
		});
	}

}

/**
 * Get translator engine object by its ID
 * @param {String} id - The ID of translator engine
 * @returns {TranslatorEngine} The translator engine
 */
 Trans.prototype.getTranslatorEngine = function(id) {
	if (TranslatorEngine.translators[id]) return TranslatorEngine.translators[id];
	if (this[id] instanceof TranslatorEngine) return this[id];
	console.warn(id+" is not a translator engine");
}



/**
 * Get currently active Translator Engine
 */
 Trans.prototype.getActiveTranslatorEngine = function() {
	var doInitLang = function() {
		var conf = confirm(t("Source & target language and the default translator engine is not yet defined for this project.\r\nPlease set it up in option menu!\r\nDo you want to set this options now?"));
		if (conf) ui.openOptionsWindow();
	}

	this.project.options 	= this.project.options || {};
	var currentPlugin 		= this.project.options.translator||sys.config.translator;
	if (typeof currentPlugin == 'undefined') return doInitLang();	
	return this.getTranslatorEngine(currentPlugin);
}

/**
 * Determines translate selection by row or by line
 * @param  {CellRange[]} [currentSelection=trans.grid.getSelected()] - Cell range to be translated
 * @param {Object} options
 * @param {Object} [options.translatorEngine=this.getActiveTranslatorEngine()] - Translator engine to be used
 */
Trans.prototype.translateSelection = async function(currentSelection, options = {}) {
	//var activeTranslator = options.translatorEngine || this.getActiveTranslatorEngine();
	this.buildIndex("Common Reference", true);
	
	var trans = this;
	currentSelection = currentSelection||trans.grid.getSelectedRange()||[[]];
	if (typeof currentSelection == 'undefined') {
		alert(t("nothing is selected"));
		return false;
	}
	if (typeof trans.translator == "undefined" || trans.translator.length < 1) {
		alert(t("no translator loaded"));
		return false;
	}
	
	var currentEngine 	= options.translatorEngine || this.getActiveTranslatorEngine();
	options.mode 		||= currentEngine.mode
	options.ignoreWhitespace ||= currentEngine.ignoreWhitespace
	console.log("translating selection ", options.mode);

	if (currentEngine.isDisabled == true) return alert(currentEngine.id+" is disabled!");

	ui.tableCornerShowLoading();
	
	var textPool = [];
	var thisData = trans.grid.getData();
	var rowPool = common.gridSelectedCells() || [];
	var tempTextPool = [];

	for (var i=0; i<rowPool.length; i++) {
		var col = rowPool[i].col;
		if (col == this.keyColumn) continue;
		tempTextPool.push(thisData[rowPool[i].row][this.keyColumn]);
	}

	if (!tempTextPool) return;
	console.log("Text pool", tempTextPool);
	var translationTable = trans.generateTranslationTableFromStrings(tempTextPool, currentEngine, options);
	console.log("Translation table:", JSON.stringify(translationTable, undefined, 2));

	translationTable = await this.processWith("translationTableFilter", translationTable, currentEngine) || translationTable;

	console.warn("Filtered translationTable", JSON.stringify(translationTable, undefined, 2));

	for (var phrase in translationTable.include) {
		textPool.push(phrase);
	}
	
	console.log(textPool);
	console.log(rowPool);
	if (rowPool.length < 1) {
		ui.tableCornerHideLoading();
		return;
	}

	
	var preTransData;
	if (currentEngine.skipReferencePair) {
		preTransData = textPool;
	} else {
		preTransData = trans.translateByReference(textPool);
	}


	console.log("Translate using : ",currentEngine.id);

	// TODO: If all result has already been translated via TM, then no need to pass into Translator engine.
	currentEngine.translate(preTransData, {
		onAfterLoading:async function(result) {
			console.log("Translation result:");
			console.log(result);
			console.log("text pool :");
			console.log(textPool);
			console.log("rowpool:");
			console.log(rowPool);
			console.log("translation result : ");

			trans.trigger("batchTranslationResult", {original:textPool, translation:result.translation, translator:currentEngine})

			var transTable = trans.generateTranslationTableFromResult(textPool, result.translation, translationTable.exclude);
			console.log("translation table : ");
			console.log(transTable);
			
			trans.applyTransTableToSelectedCell(transTable, currentSelection, undefined, options);

			ui.tableCornerHideLoading();
			trans.grid.render();
			trans.evalTranslationProgress();
			trans.textEditorSetValue(trans.getTextFromLastSelected());

		}
	});	

}

/**
 * Translate selected cell with row-by-row algorighm
 * @param  {CellRange[]} [currentSelection=trans.grid.getSelected()] - Cell range to be translated
 * @param {Object} options
 * @param {Object} [options.translatorEngine=this.getActiveTranslatorEngine()] - Translator engine to be used
 * @deprecated - Since 5.1.0
 */
 Trans.prototype.translateSelectionByRow = async function(currentSelection, options = {}) {
	options.mode = "rowByRow";
	return this.translateSelection(currentSelection, options = {})
	// console.log("translating selection by row");
	// currentSelection = currentSelection||trans.grid.getSelected()||[[]];
	// if (typeof currentSelection == 'undefined') {
	// 	alert(t("nothing is selected"));
	// 	return false;
	// }
	// if (typeof trans.translator == "undefined" || trans.translator.length < 1) {
	// 	alert(t("no translator loaded"));
	// 	return false;
	// }
	// var currentEngine = options.translatorEngine || this.getActiveTranslatorEngine();
	// ui.tableCornerShowLoading();

	// var textPool 	= [];
	// var thisData 	= trans.grid.getData();
	// var rowPool 	= common.gridSelectedCells() || [];
	// for (var i=0; i<rowPool.length; i++) {
	// 	var col = rowPool[i].col;
	// 	if (col == this.keyColumn) continue;
	// 	textPool.push(thisData[rowPool[i].row][this.keyColumn]);
	// }

	// textPool = this.processWith("textPoolFilter", textPool);

	// console.log("Text pool", textPool)
	
	// var dataString = textPool;

	// console.log(dataString);
	// console.log(rowPool);
	// if (rowPool.length < 1) {
	// 	ui.tableCornerHideLoading();
	// 	return;
	// }

	// if (currentEngine.isDisabled == true) return alert(currentEngine.id+t(" is disabled!"));
	// var preTransData;
	// if (currentEngine.skipReferencePair) {
	// 	preTransData = dataString;
	// } else {
	// 	preTransData = trans.translateByReference(dataString);
	// }


	// currentEngine.translate(preTransData, {
	// 	mode: "rowByRow",
	// 	onAfterLoading:function(result) {
	// 		console.log(">Translation result:", result);
	// 		console.log(">Row pool:", rowPool);
	// 		if (typeof result.translation !== 'undefined') {
	// 			for (var x in result.translation) {
	// 				trans.data[rowPool[x].row][rowPool[x].col] = result.translation[x];
	// 			}
	// 		}

	// 		ui.tableCornerHideLoading();
	// 		trans.grid.render();
	// 		trans.evalTranslationProgress();
	// 		trans.textEditorSetValue(trans.getTextFromLastSelected());
	// 	}
	// });

}

/**
 * Translate selected cell with line-by-line algorighm
 * @param  {CellRange[]} [currentSelection=trans.grid.getSelected()] - Cell range to be translated
 * @param {Object} options
 * @param {Object} [options.translatorEngine=this.getActiveTranslatorEngine()] - Translator engine to be used
 * @deprecated - Since 5.1.0
 */
 Trans.prototype.translateSelectionByLine = async function(currentSelection, options = {}) {
	options.mode = "lineByLine";
	return this.translateSelection(currentSelection, options = {})

	// var trans = this;
	// console.log("translating selection by line", options.mode);
	// currentSelection = currentSelection||trans.grid.getSelected()||[[]];
	// if (typeof currentSelection == 'undefined') {
	// 	alert(t("nothing is selected"));
	// 	return false;
	// }
	// if (typeof trans.translator == "undefined" || trans.translator.length < 1) {
	// 	alert(t("no translator loaded"));
	// 	return false;
	// }
	
	// var currentEngine = options.translatorEngine || this.getActiveTranslatorEngine();
	// if (currentEngine.isDisabled == true) return alert(currentEngine.id+" is disabled!");

	// ui.tableCornerShowLoading();
	
	// var textPool = [];
	// var thisData = trans.grid.getData();
	// var rowPool = common.gridSelectedCells() || [];
	// var tempTextPool = [];

	// for (var i=0; i<rowPool.length; i++) {
	// 	var col = rowPool[i].col;
	// 	if (col == this.keyColumn) continue;
	// 	tempTextPool.push(thisData[rowPool[i].row][this.keyColumn]);
	// }

	// if (!tempTextPool) return;
	// console.log("Text pool", tempTextPool);
	// var translationTable = trans.generateTranslationTableFromStrings(tempTextPool, currentEngine, options);
	// console.log("Translation table:", translationTable);

	// translationTable = await this.processWith("translationTableFilter", translationTable, currentEngine) || translationTable;

	// console.warn("Filtered translationTable", translationTable);

	// for (var phrase in translationTable.include) {
	// 	textPool.push(phrase);
	// }
	
	// console.log(textPool);
	// console.log(rowPool);
	// if (rowPool.length < 1) {
	// 	ui.tableCornerHideLoading();
	// 	return;
	// }

	
	// //var preTransData = trans.translateByReference(textPool);
	// var preTransData;
	// if (currentEngine.skipReferencePair) {
	// 	preTransData = textPool;
	// } else {
	// 	preTransData = trans.translateByReference(textPool);
	// }


	// console.log("Translate using : ",currentEngine.id);

	// currentEngine.translate(preTransData, {
	// 	onAfterLoading:async function(result) {
	// 		console.log("Translation result:");
	// 		console.log(result);
	// 		console.log("text pool :");
	// 		console.log(textPool);
	// 		console.log("rowpool:");
	// 		console.log(rowPool);
	// 		console.log("translation result : ");

	// 		trans.trigger("batchTranslationResult", {original:textPool, translation:result.translation, translator:currentEngine})

	// 		var transTable = trans.generateTranslationTableFromResult(textPool, result.translation, translationTable.exclude);
	// 		console.log("translation table : ");
	// 		console.log(transTable);
			
	// 		trans.applyTransTableToSelectedCell(transTable, currentSelection, undefined, options);

	// 		ui.tableCornerHideLoading();
	// 		trans.grid.render();
	// 		trans.evalTranslationProgress();
	// 		trans.textEditorSetValue(trans.getTextFromLastSelected());

	// 	}
	// });
	
}

Trans.prototype.translateSelectionAsOneLine = async function(currentSelection, options = {}) {
	console.log("Merge to one line then translate");
	currentSelection = currentSelection||trans.grid.getSelectedRange()||[[]];
	if (typeof currentSelection == 'undefined') {
		alert(t("nothing is selected"));
		return false;
	}
	if (typeof trans.translator == "undefined" || trans.translator.length < 1) {
		alert(t("no translator loaded"));
		return false;
	}
	var currentEngine = options.translatorEngine || this.getActiveTranslatorEngine();
	
	var originalText = this.getSelectedOriginalTexts(currentSelection);
	var sourceText = this.getSelectedOriginalTextsAsOneLine(currentSelection);
	var selectedRows = common.gridSelectedRows(currentSelection);
	var rowIndex = {};
	for (var i=0; i<selectedRows.length; i++) {
		rowIndex[selectedRows[i]] = i;
	}
	console.log("Row index:", rowIndex);
	var data = this.getData();

	console.log("Original text:", originalText);
	ui.tableCornerShowLoading();
	var preTransData;
	if (currentEngine.skipReferencePair) {
		preTransData = sourceText;
	} else {
		preTransData = trans.translateByReference(sourceText);
	}

	console.log("Translating", preTransData);
	currentEngine.translate(preTransData, {
		mode: "rowByRow",
		onAfterLoading: (result) => {
			if (typeof result.translation !== 'undefined') {
				console.log("result translation:", result.translation);
				var formattedResult = common.cloneFormatting(originalText, result.translation.join(" "));
				console.log("Formatted result:", formattedResult);

				console.log("Write back the translated result according to the row");
				var selectedCells = common.gridSelectedCells(currentSelection)
				for (var i=0; i<selectedCells.length; i++) {
					if (selectedCells[i].col == this.keyColumn) continue;
					data[selectedCells[i].row][selectedCells[i].col] = formattedResult[rowIndex[selectedCells[i].row]];
				}

			}
			
			ui.tableCornerHideLoading();
			trans.grid.render();
			trans.evalTranslationProgress();
			trans.textEditorSetValue(trans.getTextFromLastSelected());
		}
	});
}

/**
 * Translate using all availables translator into their corresponding default column.
 * @param {CellRange} currentSelection 
 */
 Trans.prototype.translateSelectionIntoDef =function(currentSelection) {
	/*
		this function will translate using all available translator into their correspinding
		default column.
	*/
	console.log("translating selection into default coloumn");
	currentSelection = currentSelection||trans.grid.getSelected()||[[]];
	if (typeof currentSelection == 'undefined') {
		alert(t("nothing is selected"));
		return false;
	}
	if (typeof trans.translator == "undefined" || trans.translator.length < 1) {
		alert(t("no translator loaded"));
		return false;
	}
	
	
	var textPool = [];
	var thisData = trans.grid.getData();
	
	var rowPool = [];
	for (var index=0; index<currentSelection.length; index++) {
		for (var row=currentSelection[index][0]; row<=currentSelection[index][2]; row++) {
			rowPool.push(row);
			textPool.push(thisData[row][0]);
		}
	}
	//var dataString = textPool.join($DV.config.lineSeparator);
	var dataString = textPool;

	console.log(dataString);
	console.log(rowPool);

	for (var i=0; i<trans.translator.length; i++ ) {
		console.log(i);
		var currentPlugin = trans.translator[i];
		var thisTranslator = this.getTranslatorEngine(currentPlugin);
		if (thisTranslator.isDisabled == true) continue;
		if (typeof thisTranslator.targetColumn == "undefined") {
			console.warn("Skipping. Reason : TargetColumn property is not set for engine ", currentPlugin)
			continue;
		}
		
		var preTransData = trans.translateByReference(dataString);
		thisTranslator.translate(preTransData, {
			onAfterLoading:function(result) {
				if (typeof result.translation !== 'undefined') {

					for (var x in result.translation) {
						trans.data[rowPool[x]][thisTranslator.targetColumn] = result.translation[x];
					}
				}
				
				//trans.refreshGrid();
				trans.grid.render();
				trans.evalTranslationProgress();
				
			}
		});
	}
	
}

/**
 * Apply translation table into selected cell
 * @param {Object} transTable - Translation table formatted object
 * @param {CellRange} [currentSelection=trans.grid.getSelected()] - Cell range to put translation into
 * @param {String[][]} [transData=this.data] - Two dimensional array representing the grid
 * @param {Object} [options] 
 * @param {Number} [options.indexKey=0] - Index of the key column
 */
 Trans.prototype.applyTransTableToSelectedCell = function(transTable, currentSelection, transData, options) {
	/*
		transTable = translation table format;
		selectedCell = array of cell selection
		transData = either trans.data / trans.project.files['file'].data
	*/

	transData 			= transData||trans.data; // current selected file
	currentSelection 	= currentSelection||trans.grid.getSelected()||[[]];
	options 			= options||{};
	options.indexKey 	= options.indexKey||0;
	if (typeof currentSelection == 'undefined') {
		alert(t("nothing is selected"));
		return false;
	}

	//console.log("applyTransTableToSelectedCell", arguments);
	var rowPool = common.gridSelectedCells(currentSelection) || [];
	if (options.mode == "rowByRow") {
		for (let i=0; i<rowPool.length; i++) {
			let col = rowPool[i].col;
			let row = rowPool[i].row;
			if (col == this.keyColumn) continue;
	
			if (Array.isArray(transData[row]) == false) continue;
			transData[row][col] = transTable[transData[row][options.indexKey]];
		}
	} else {
		for (let i=0; i<rowPool.length; i++) {
			let col = rowPool[i].col;
			let row = rowPool[i].row;
			if (col == this.keyColumn) continue;
	
			if (Array.isArray(transData[row]) == false) continue;
			//console.log("---looking for", JSON.stringify(transData[row][options.indexKey]));
			//console.log("transtable:", JSON.stringify(transTable, undefined, 2))
			transData[row][col] = this.translateTextByLine(transData[row][options.indexKey], transTable);
		}
	}
}


/**
 * Translate selected row with translation pane. (Live translator)
 * @param {Number} row 
 * @param {Number} col 
 * @returns {Boolean}
 */
 Trans.prototype.translateSelectedRow = function(row, col) {
	if (typeof row == 'undefined') return false;
	if (trans.config.autoTranslate == false) return false;
	//if ($("#translationPane").attr("src") == "") return false;
	col = col||0;
	
	var currentText = trans.data[row][col];
	if (!currentText) return false;
	
	var translatorWindow = $("#translationPane")[0].contentWindow;
	if (ui.windows['translator']) {
		translatorWindow = ui.windows['translator'];
	}
	if (!translatorWindow.translator) return t("unable to load translator window");
	
	translatorWindow.translator.translateAll(currentText);
	return true;
}

/**
 * Get translation by index of the translation portlet (Live translator)
 * @param {Number} index - Index of the portlet
 * @returns {String} Translated text
 */
 Trans.prototype.getTranslationByIndex = function(index) {
	if (typeof index == 'undefined') return false;
	//if ($("#translationPane").attr("src") == "") return false;
	
	var translatorWindow = $("#translationPane")[0].contentWindow;
	if (ui.windows['translator']) {
		translatorWindow = ui.windows['translator'];
	}
	return translatorWindow.$(".mainPane .portlet").eq(index).find(".portlet-content").text();
}

/**
 * Import translation from other .trans file
 * @param {String|Trans} refPath - path to the Trans File or an object content of transFile
 * @param {Object} options 
 * @param {Number} [options.targetColumn=1] - target column to write for
 * @param {Boolean} [options.overwrite=false] - Whether to overwrite the destination cell or not
 * @param {String[]} [options.files] - imported selected file list
 * @param {String[]} [options.destination] - destination file list
 * @param {lineByLine|rowByRow|contextTrans|copyByRow} [options.compareMode=contextTrans] - Copy method
 */
 Trans.prototype.importTranslation = async function(refPath, options) {
	console.log("importTranslation", arguments);

	if (typeof refPath == 'undefined') return trans.alert(t("Reference path cannot empty!"));
	options = options || {};
	options.targetColumn 	= options.targetColumn||1;
	options.overwrite 		= options.overwrite||false;
	options.files 			= options.files||[]; // imported selected file list
	options.destination		= options.destination||[]; // destination file list
	options.compareMode		= options.compareMode||0; // context to context
	options.ignoreLangCheck = true; // always ignore language check!
	options.destinationMode	= options.destinationMode || "selected";
	options.caseInsensitive ||= false;

	console.log("refPath & options : ");
	console.log(arguments);
	//return true;
	// selecting file is required
	if (!trans.getSelectedId()) {
		trans.selectFile($(".panel-left .fileList .data-selector").eq(0));
	}

	var lowerCaseText = undefined;
	if (options.caseInsensitive) {
		lowerCaseText = function(text) {
			return text.toLowerCase().replaceAll("\r", "");
		}
	} 

	var applyImportedTranslation = async (loadedData) => {
		console.log("Applying translation");

		ui.loadingProgress(30, t("Collecting translation refference"));
		await ui.log("Collecting translation refference");
		
		var refTranslation = {};
		var tempIndexes;
		if (options.compareMode == 'lineByLine') {
			refTranslation = trans.generateTranslationTableLine(loadedData.project.files, options);
			let targetFiles;
			if (options.destinationMode == "selected") {
				targetFiles = this.getCheckedFiles()
			}
			tempIndexes = this.buildIndexes(targetFiles, true, {customFilter:lowerCaseText});
		} else if (options.compareMode == 'rowByRow') {
			refTranslation = trans.generateTranslationTable(loadedData.project.files, options);
			let targetFiles;
			if (options.destinationMode == "selected") {
				targetFiles = this.getCheckedFiles()
			}
			tempIndexes = this.buildIndexes(targetFiles, false, {customFilter:lowerCaseText});
		} else if (options.compareMode == 'contextTrans') {
			refTranslation = trans.generateContextTranslationPair(loadedData.project.files, options);
		}

		//console.log("reference translation is : ", refTranslation);
		//console.log(refTranslation);
		loadedData = trans.mergeReference(loadedData);
		
		var numData = Object.keys(refTranslation).length
		await ui.log(`Processing ${numData} reference(s)`);
		ui.loadingProgress(50, t("Applying translation!"));
		var count = 0;
		var lastProgress = 50;
		if (options.compareMode == 'lineByLine') { // line by line
			for (let key in refTranslation) {
				let origKey = key;
				if (options.caseInsensitive) key = lowerCaseText(key);
				trans.findAndInsertWithIndexes(
					key, 
					refTranslation[origKey], 
					options.targetColumn,
					tempIndexes,
					{
						overwrite	:options.overwrite,
						files		:options.destination,
						lineByLine	:true,
						customFilter:lowerCaseText
					});
				ui.loadingProgress(50+(count/numData*50));
				count++;
			}			
		} else if (options.compareMode == 'rowByRow') { // row by row
			console.log("Row by Row translation");
			for (let key in refTranslation) {
				let origKey = key;
				if (options.caseInsensitive) key = lowerCaseText(key);
				trans.findAndInsertWithIndexes(	
					key, 
					refTranslation[origKey], 
					options.targetColumn, 
					tempIndexes,
					{
						overwrite	:options.overwrite,
						files		:options.destination,
						insensitive	:true,
						customFilter:lowerCaseText
					});
				var thisProgress = Math.round(50+(count/numData*50))
				if (thisProgress > lastProgress) {
					ui.loadingProgress(50+(count/numData*50));
					await ui.log(`Handled: ${count}/${numData}`);
					lastProgress = thisProgress;
				}
				
				count++;
			}
		} else if (options.compareMode == 'contextTrans') {
			console.log("Translation by context");
			for (var key in refTranslation) {
				trans.findAndInsertByContext(key, refTranslation[key], options.targetColumn, {
					overwrite:options.overwrite,
					files:options.destination
				});

				ui.loadingProgress(50+(count/numData*50));
				count++;
			}
		} else if (options.compareMode == 'copyByRow') {
			this.copyTranslationToRow(loadedData.project.files, options.targetColumn, options);
		}
		/**
		 * Triggered after import translation process is done
		 * @param  {Object} options
		 * @param  {Object} options.options
		 * @param  {Object} options.loadedData
		 * @param  {Object} options.refTranslation
		 */
		trans.trigger("onAfterImportTranslations", {
			options:options, 
			loadedData:loadedData,
			refTranslation:refTranslation
		});

		await common.wait(20)
		trans.evalTranslationProgress();
		trans.refreshGrid();
		ui.loadingProgress(100, "Done!");
		ui.loadingEnd();
		
	}
	
	ui.showLoading();
	ui.loadingProgress(0, t("Importing translation"));
	await common.wait(200);

	
	if (typeof refPath == 'object' && typeof refPath.project !== 'undefined') {
		// refference path already loaded
		console.log("refPath is an object : ");
		await applyImportedTranslation(refPath);
		
		return true;
		
	}
	console.log("Opening "+refPath);
	
	
	fs.readFile(refPath, async function (err, data) {
		if (err) {
			console.log("error opening file : "+refPath);
			data = data.toString();
			if (typeof options.onFailed =='function') options.onFailed.call(trans, data);

			throw err;
		} else {
			ui.loadingProgress(20, t("Parsing data"));
			await ui.log("Parsing data");
			await common.wait(200);

			data = data.toString();
			//console.log(data);
			var jsonData = JSON.parse(data);
			console.log("Result data : ");
			console.log(jsonData);
			applyImportedTranslation(jsonData);
			console.log("Done!");
			if (typeof options.onSuccess == 'function') options.onSuccess.call(trans, jsonData);
			trans.isOpeningFile = false;
		}
	});	
}


Trans.prototype.translateAllBySelectedCells = async function(currentSelection, fileId, options) {
	currentSelection = currentSelection||this.grid.getSelectedRange()||[[]];
	fileId = fileId || this.getSelectedId();
	options = options || {};

	ui.showBusyOverlay();
	await common.wait(100);
	var targetFiles = this.getAllFilesExcept([fileId]);
	//console.log("Target files:", targetFiles);

	var selectedTransTable = this.generateSelectedTranslationTable(currentSelection, fileId, options);
	console.log("Selected transtable", selectedTransTable);
	var tempIndexes = this.buildIndexes(targetFiles);
	var found = [];
	for (var keyword in selectedTransTable) {
		var foundCells = this.getFromIndexes(keyword, tempIndexes);
		if (!foundCells) continue;
		for (var y=0; y<foundCells.length; y++) {
			var foundCell = foundCells[y];
			var targetData = this.getData(foundCell.file);

			for (var i=0; i<selectedTransTable[keyword].length; i++) {
				var cellInfo = selectedTransTable[keyword][i];
				var oldValue = targetData[foundCell.row][cellInfo.col];
				if (cellInfo.col == this.keyColumn) continue;
				targetData[foundCell.row][cellInfo.col] = cellInfo.value;
				//console.log("Setting up file", foundCell.file, "row", foundCell.row, "col", cellInfo.col, "with value:", cellInfo.value);
				found.push({
					file:foundCell.file,
					row:foundCell.row,
					col:cellInfo.col,
					oldValue:oldValue,
					newValue:cellInfo.value
				})
			}
		}
	}
	ui.hideBusyOverlay();
	return found;
}

/**
 * Get texts from selected cells on the grid
 * @since 4.7.16
 * @param {*} currentSelection 
 * @param {*} fileId 
 * @returns {String[]} Array of texts from the selected cells
 */
Trans.prototype.getSelectedTexts = function(currentSelection, fileId) {
	currentSelection = currentSelection||this.grid.getSelectedRange()||[[]];
	fileId = fileId || this.getSelectedId();
	var data = this.getData(fileId);
	var selectedCells = common.gridSelectedCells(currentSelection);

	var result = [];
	for (var i=0; i<selectedCells.length; i++) {
		result.push(
			data[selectedCells[i].row][selectedCells[i].col] || ""
		)
	}
	return result;
}

/**
 * Get text from the grid
 * @param {Number} row - Row index
 * @param {Number} column - Column index
 * @param {String} [file] - File id
 * @returns {String} - Text of the selected row, column and file
 */
Trans.prototype.getText = function(row, column, file) {
	if (!file) {
		return this.data?.[row]?.[column]
	}
	const obj = trans.getObjectById(file);
	return obj?.[row]?.[column]
}

/**
 * Get cell comments on a coordinate
 * @param {Number} row - Row index
 * @param {Number} column - Column index
 * @param {String} [file] - File id
 * @returns {String} - Comment of the selected row, column and file
 */
Trans.prototype.getCellComment = function(row, column, file) {
	let obj
	if (!file) {
		obj = this.getSelectedObject();
	} else {
		obj = this.getObjectById(file)
	}

	if (!obj.comments) return;
	return obj.comments?.[row]?.[column];

}

/**
 * Select original texts from grid selection
 * @param {CellRange[]|Number[][]} currentSelection - Current selection
 * @param {String} fileId - File id
 * @returns {String[]} Array of string of the selected original texts
 */
Trans.prototype.getSelectedOriginalTexts = function(currentSelection, fileId) {
	currentSelection = currentSelection||this.grid.getSelectedRange()||[[]];
	fileId = fileId || this.getSelectedId();
	var data = this.getData(fileId);
	var selectedCells = common.gridSelectedCells(currentSelection);

	var result = [];
	for (var i=0; i<selectedCells.length; i++) {
		result.push(
			data[selectedCells[i].row][this.keyColumn] || ""
		)
	}
	return result;
}

/**
 * Get texts from selected cells and merge them into one line
 * @since 4.7.16
 * @param {*} currentSelection 
 * @param {*} fileId 
 * @returns {String} Text of the selected cell in one line
 */
Trans.prototype.getSelectedTextsAsOneLine = function(currentSelection, fileId) {
	var texts = this.getSelectedTexts(currentSelection, fileId) || [];
	var merged = texts.join(" ");
	return merged.replaceAll("\r", "").replaceAll("\n", " ");
}

/**
 * Get original texts from selected cells and merge them into one line
 * @since 4.7.16
 * @param {*} currentSelection 
 * @param {*} fileId 
 * @returns {String} Original text of the selected cell in one line
 */
 Trans.prototype.getSelectedOriginalTextsAsOneLine = function(currentSelection, fileId) {
	currentSelection = currentSelection||this.grid.getSelectedRange()||[[]];
	fileId = fileId || this.getSelectedId();
	var data = this.getData(fileId);

	var rows = common.gridSelectedRows()

	var result = []
	for (var i=0; i<rows.length; i++) {
		result.push(
			data[rows[i]][this.keyColumn] || ""
		)
	}
	return result.join(" ");
}



// ==================================================================
// PUT DOM RELATED CODE HERE
// ==================================================================
/**
 * returns related key ID from trans.project.files
 * returning false when error
 * @returns {String|False} The currently seleceted ID
 */
 Trans.prototype.getSelectedId = function() {
	// returning false when error
	// returns related key ID from trans.project.files

	// getting the value from trans.project.selectedId is faster than DOM
	//if (trans.project?.selectedId) return trans.project.selectedId;
	if (!trans.project) return false;
	return trans.project?.selectedId

	/*
	if ($(".fileList .selected").length == 0 ) return false;
	return $(".fileList .selected").data("id");
	*/
}

/**
 * Get selected row's context
 * @param {Number} rowNumber - Row id to look for
 * @returns {String} Context
 */
 Trans.prototype.getSelectedContext = function(rowNumber) {
	rowNumber = rowNumber || trans.lastSelectedCell[0]
	var context = trans.getSelectedObject().context;
	try {
		return context[rowNumber]
	} catch (e) {
		context[rowNumber] = [];
		return context[rowNumber];
	}
}

/**
 * Get selected row's parameters
 * @returns {Object} parameters of the selected row
 */
 Trans.prototype.getSelectedParameters = function() {
	if (!trans.lastSelectedCell) return;
	var rowNumber = trans.lastSelectedCell[0]

	var obj =trans.getSelectedObject()
	obj.parameters = obj.parameters || [];
	obj.parameters[rowNumber] = obj.parameters[rowNumber] || []
	return obj.parameters[rowNumber]
}

/**
 * Get parameters by row id & file id
 * @param {Number} row - The row id
 * @param {String} file - The file id
 * @returns {Object|false} parameters of the selected row or false if no parameter is found
 */
 Trans.prototype.getParamatersByRow = function(row, file) {
	file = file || this.getSelectedId();
	if (typeof row !== "number") return false;

	var thisObj = trans.getObjectById(file);
	if (!thisObj?.parameters) return false;
	return thisObj.parameters[row];
}

/**
 * Get row info text from file object's parameters
 * @param {Number} row - Row index
 * @param {Boolean} [full=false] - Whether to display full result or not
 * @param {String} [file=Trans.getSelectedId()] - Target file object
 * @returns {String} row info
 */
Trans.prototype.getRowInfoText = function(row, full=false, file="") {
	file = file || this.getSelectedId();
	const thisParam = this.getParamatersByRow(row, file);

	if (!thisParam) return "";
	const rowInfoReference = (this.getOption("gridInfo")||{}).referenceName || "Actor Reference";

	var result;
	if (full) {
		result = [];
		for (let i in thisParam) {
			if (!thisParam[i]) continue;
			if (typeof thisParam[i] !== "object") continue;
			if (thisParam[i].rowInfoText) result.push(this.translateByReference(thisParam[i].rowInfoText, false, rowInfoReference));
		}

		let uniqueItems = [...new Set(result)]
		return uniqueItems.join(", ")
	}

	result = "";
	for (let i in thisParam) {
		if (!thisParam[i]) continue;
		if (typeof thisParam[i] !== "object") continue;
		if (result) return result+"++";
		if (thisParam[i].rowInfoText) result = this.translateByReference(thisParam[i].rowInfoText, false, rowInfoReference);
	}
	return result;
}

/**
 * Get selected key text from row
 * Usually cell 0
 * @param {*} rowNumber - Row id
 * @returns {String|undefined} key text
 */
 Trans.prototype.getSelectedKeyText = function(rowNumber) {
	rowNumber = rowNumber || trans.lastSelectedCell[0]
	try {
		var data = trans.getSelectedObject().data;
		return data[rowNumber][trans.keyColumn]
	} catch (e) {
		// do nothing
	}
}

/**
 * returns related object from trans.project.files[currently selected]
 * returning false when error
 * @returns {Object|false} 
 */
 Trans.prototype.getSelectedObject = function() {
	// returning false when error
	// returns related object from trans.project.files[currently selected]
	if ($(".fileList .selected").length == 0 ) return false;
	var currentID = trans.getSelectedId();
	return trans.project.files[currentID];
}

/**
 * Get file object by its id
 * @param {String} id - The file id
 * @returns {Object} the file object. Equal to trans.project.files[id]
 */
 Trans.prototype.getObjectById = function(id) {
	if (!id) return;
	try {
		return trans.project.files[id]
	} catch (e){
		console.warn(e);
		return;
	}
}

/**
 * Get list of the checked file(s) on the left pane
 * @returns {String[]} Array of the checked file id
 */
 Trans.prototype.getCheckedFiles = function() {
	var result = [];
	var checkbox = $(".fileList .data-selector .fileCheckbox:checked");
	for (var i=0; i<checkbox.length; i++) {
		result.push(checkbox.eq(i).attr("value"));
	}
	return result;
	
}

/**
 * Get selected objects
 * @returns {Object} Selected object
 */
 Trans.prototype.getCheckedObjects = function() {
	var result = {};
	var checkbox = $(".fileList .data-selector .fileCheckbox:checked");
	for (var i=0; i<checkbox.length; i++) {
		var id = checkbox.eq(i).attr("value");
		result[id] = this.getObjectById(id)
	}
	return result;
}


/**
 * Get all file ids on the project
 * @param {Object} [obj=this.project.files] - list of file objects
 * @param {Boolean} [excludeReference=false] - wether to exclude reference or not
 * @returns {String[]}
 */
 Trans.prototype.getAllFiles = function(obj, excludeReference) {
	var result = [];
	obj = obj||trans?.project?.files;
	if (typeof obj == 'undefined') return result;
	if (obj?.project?.files) {
		obj = obj?.project?.files;
	}
	if (excludeReference) {
		for (let file in obj ) {
			if (obj[file]?.dirname == "*") continue; // skip reference
			result.push(file);
		}
		return result;
	}

	for (let file in obj ) {
		result.push(file);
	}
	return result;
}

/**
 * Get all file ids on the project, except filteredIds
 * @since 4.7.17
 * @param {String[]} filteredIds - list of exceptions
 * @param {Object} [obj=this.project.files] - list of file objects
 * @returns {String[]}
 */
Trans.prototype.getAllFilesExcept = function(filteredIds, obj) {
	if (typeof filteredIds == "string") filteredIds = [filteredIds];
	filteredIds = filteredIds || [];
	obj = obj||trans.project.files;
	var result = [];
	for (var file in obj ) {
		if (filteredIds.includes(file)) continue;
		result.push(file);
	}
	return result;
}

/**
 * Get all files with 100% progress
 * @param {*} [obj=this.project.files] - list of file objects
 * @returns {String[]} List of the files
 */
 Trans.prototype.getAllCompletedFiles = function(obj) {
	var result = [];
	obj = obj||this.project.files;
	if (typeof obj == 'undefined') return result;
	for (var file in obj ) {
		//console.log(file, obj[file]);
		if (typeof obj[file] !== "object") continue;
		if (typeof obj[file].progress !== "object") continue;
		if (obj[file].progress.percent == 100) {
			result.push(file);
		}
		
	}
	return result;
}

/**
 * Get all files with less than 100% progress
 * @param {*} [obj=this.project.files] - list of file objects
 * @returns {String[]} List of the files
 */
 Trans.prototype.getAllIncompletedFiles = function(obj) {
	var result = [];
	obj = obj||this.project.files;
	if (typeof obj == 'undefined') return result;
	for (var file in obj ) {
		//console.log(file, obj[file]);
		if (typeof obj[file] !== "object") continue;
		if (typeof obj[file].progress !== "object") continue;
		if (obj[file].progress.percent < 100) {
			result.push(file);
		}
		
	}
	return result;
}

/**
 * Get all files marked as completed
 * @param {*} [obj=this.project.files] - list of file objects
 * @returns {String[]} List of the files
 * @since 4.4.5
 */
Trans.prototype.getAllMarkedAsCompleted = function(obj) {
	var result = [];
	obj = obj||this.project.files;
	if (typeof obj == 'undefined') return result;
	for (var file in obj ) {
		if (typeof obj[file] !== "object") continue;
		if (typeof obj[file].progress !== "object") continue;
		if (obj[file].isCompleted) {
			result.push(file);
		}
		
	}
	return result;
}

/**
 * Get attachment content by ID
 * @param {String} id - ID of the attachment
 * @since 6.3.27
 * @returns {undefined|String} - Attachment content
 */
Trans.prototype.getAttachmentContent = function(id) {
	if (!id) return;
	if (!trans.project?.attachments?.[id]) return;
	return trans.project.attachments[id].data;
}

/**
 * Scroll horizontally to the selected column.
 * The column in argument 1 will be displayed next to the frozen cols
 * @param {*} col - Column index to be shown
 */
Trans.prototype.scrollHToCol = function(col) {
	var $container = $("#table .ht_master .wtHolder")
	if (!col) return $container[0].scrollLeft = 0
	var $colls = $("#table .ht_master .wtHolder colgroup col");
	//var margin = $colls.eq(0).outerWidth()
	var scrollWidth = 0
	for (var i=1; i<col; i++) { 
		scrollWidth += $colls.eq(i+1).outerWidth();
	}
	
	console.log(scrollWidth)
	$container[0].scrollLeft = scrollWidth;
}

// ==============================================================
// SEARCH
// ==============================================================
/**
 * Go to cell. Will select the cell and scroll the viewport to the cell.
 * @param {Number} row 
 * @param {Number} col 
 * @param {String|JQuery} fileId 
 */
 Trans.prototype.goTo = function(row, col, fileId) {
	console.log(arguments);
	fileId = fileId || this.getSelectedId()
	/*
	trans.selectFile(context, {
		onDone:function() {
			trans.grid.selectCell(row,col,row,col);
		}
	});
	*/
	// commit any change on current cell
	this.grid.deselectCell();
	
	if (fileId !== this.getSelectedId()) {
		var $selected = this.selectFile(fileId);
		//$($selected)[0].scrollIntoView({behavior: "smooth"});
		$($selected)[0].scrollIntoView({block:"center"});
	}

	this.grid.selectCell(row,col,row,col);
	this.grid.scrollViewportTo(row,col);
	this.scrollHToCol(col);
	//setTimeout (function() {trans.grid.selectCell(row,col,row,col)}, 1000);
	
}

/**
 * Get the last selected Cell
 * @returns {Number[]} Array of row and column
 * Return `[0,0]` if no cell is selected;
 * @since 4.4.4
 */
Trans.prototype.getLastSelectedCell = function() {
	return this.lastSelectedCell;
}

/**
 * Go to the next untranslated cell
 * @returns {Boolean} Whether the operation is successful or not
 */
Trans.prototype.goToNextUntranslated = function() {
	var selectedCell = this.getLastSelectedCell();
	var data = this.getCurrentData();
	var starting = selectedCell[0]+1;
	if (starting >= data.length) starting = 0;
	for (var rowId=starting; rowId<data.length; rowId++) {
		if (this.rowHasTranslation(data[rowId])) continue;
		break;
	}
	if (rowId >= data.length) rowId = data.length - 1;
	return this.goTo(rowId, selectedCell[1]);
}

/**
 * Go to the previous untranslated cell
 * @returns {Boolean} Whether the operation is successful or not
 */
Trans.prototype.goToPreviousUntranslated = function() {
	var selectedCell = this.getLastSelectedCell();
	var data = this.getCurrentData();
	var starting = selectedCell[0]-1;
	if (starting <= 0) starting = data.length - 1;
	console.log("starting", starting);
	for (var rowId=starting; rowId>=0; rowId--) {
		if (this.rowHasTranslation(data[rowId])) continue;
		break;
	}
	console.log("Move to row", rowId);
	if (rowId < 0) rowId = 0;
	return this.goTo(rowId, selectedCell[1]);
}

/**
 * Search for some keyword
 * @param {String} keyword - Keyword
 * @param {Object} options 
 * @param {Boolean} [options.caseSensitive] - Whether the search is in case sensitive mode or not
 * @param {Boolean} [options.lineMatch] - Whether the search is with the line match mode or not
 * @param {Boolean} [options.isRegexp] - Whether the keyword is a regexp or not
 * @returns {SearchResult} the search result
 */
 Trans.prototype.search = function(keyword, options) {
	var globToRegExp = require('glob-to-regexp');
	console.log("entering trans.search", arguments);

	if (typeof keyword == "undefined") return null;
	if (typeof keyword.length <=1) return "Keyword too short!";
	if (typeof trans.project == "undefined") return null;
	if (typeof trans.project.files == "undefined") return null;
	
	
	options = options|| {};
	options.caseSensitive 	= options.caseSensitive||false;
	options.lineMatch 		= options.lineMatch||false;
	options.isRegexp 		= options.isRegexp||false;

	options.searchLocations = options.searchLocations || [];
	if (options.searchLocations.length == 0) options.searchLocations = ['grid'];
	
	if (options.lineMatch) options.searchInContext = false;
	
	if (Array.isArray(options.files) == false) {
		options.files = [];
		for (let file in trans.project.files) {
			options.files.push(file);
		}
	}
	
	
	if (options.caseSensitive == false && options.isRegexp == false) {
		keyword = keyword.toLowerCase();
	}
	
	var start = new Date().getTime();
	
	/**
	 * @typedef SearchResult
	 * @property {String} keyword - The keyword used
	 * @property {Number} count - Total numbers of the result
	 * @property {Boolean} isRegExp - Whether the keyword is regular expression or not
	 * @property {Number} executionTime - Execution time in ms
	 * @property {Object} files - list of the search result by file id
	 * @property {String} files.fullString - Full string of the result
	 * @property {Number} files.row - Row id
	 * @property {Number} files.col - Column id
	 * @property {cell|context|comment} files.type - type of the search result
	 * @property {Number} files.lineIndex - The line index
	 */

	var result = {
		keyword:keyword,
		count:0,
		isRegexp:options.isRegexp,
		executionTime:0,
		files:{}
	};
	
	// check if regexp is valid
	var keywordExp;
	if (options.isRegexp) {
		keywordExp = common.evalRegExpStr(keyword);
		if (keywordExp == false) {
			alert(keyword+t(" is not a valid javascript's regexp!\r\nFind out more about Javascipt's Regular Expression at :\r\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions"));
			return result;
		}
	} else if (keyword.includes("*")) {
		// glob style keyword
		try {
			var fixedKeywordExp = globToRegExp(keyword).toString().replace(/\/\^(.*?)\$\//, '$1');
			keywordExp = new RegExp(fixedKeywordExp, 'i'); // insensitive
			console.log("Glob style keyword detected", keywordExp);
		} catch (e) {
			console.warn('Error when converting glob pattern to RegExp');
		}

	}
	
	//line match algorithm
	if (options.lineMatch) {
		for (let cont in options.files) {
			let file = options.files[cont];
			if (Array.isArray(trans.project.files[file].data) == false) continue;
			let currentFile = trans.project.files[file].data;
			for (let row=0; row<currentFile.length; row++) {
				if (currentFile[row].length == 0) continue;
				for (let col=0; col<currentFile[row].length; col++) {
					if (typeof currentFile[row][col] !== "string") continue;
					
					
					if (keywordExp) {
						// regular expression search
						if (keywordExp.test(currentFile[row][col])) {
							// match found
							let lineIndex = common.lineIndexRegExp(currentFile[row][col], keywordExp);
							result.files[file] = result.files[file]||[];
							result.files[file].push({
								'fullString':currentFile[row][col],
								'row':row,
								'col':col,
								'type':'cell',
								'lineIndex':lineIndex
							});
							result.count++;	
							
							break;							
						}
						
					} else {
						// normal search
						if (options.caseSensitive) {
							if (currentFile[row][col].indexOf(keyword) == -1) continue;
						} else {
							if (currentFile[row][col].toLowerCase().indexOf(keyword) == -1) continue;
						}
						
						let lineIndex = common.lineIndex(currentFile[row][col], keyword, options.caseSensitive);
						if (lineIndex != -1) {
							// match found
							result.files[file] = result.files[file]||[];
							result.files[file].push({
								'fullString':currentFile[row][col],
								'row':row,
								'col':col,
								'type':'cell',
								'lineIndex':lineIndex
							});
							result.count++;	
							
							break;
						}						
					}

				}
			}
		}	
		
		let end = new Date().getTime();
		result.executionTime = end - start;
		return result;		
	}
	
	console.log("Search location:", options.searchLocations);
	// common algorithm
	if (options.searchLocations.includes("grid")) {
		//for (var file in trans.project.files) {
		for (let cont in options.files) {
			let file = options.files[cont];
			if (Array.isArray(trans.project.files[file].data) == false) continue;
			let currentFile = trans.project.files[file].data;
			for (let row=0; row<currentFile.length; row++) {
				if (currentFile[row].length == 0) continue;
				for (let col=0; col<currentFile[row].length; col++) {
					if (typeof currentFile[row][col] !== "string") continue;
					
					
					if (keywordExp) {
						//console.log("regexp search:", keywordExp, currentFile[row][col], keywordExp.test(currentFile[row][col]));
						//console.log(typeof keywordExp);
						// regular expression search
						if (keywordExp.test(currentFile[row][col])) {
							// match found
							result.files[file] = result.files[file]||[];
							result.files[file].push({
								'fullString':currentFile[row][col],
								'row':row,
								'col':col,
								'type':'cell'
							});
							result.count++;	
							
							break;							
						}
						
					} else {			
						
						if (options.caseSensitive) {
							if (currentFile[row][col].indexOf(keyword) == -1) continue;
						} else {
							if (currentFile[row][col].toLowerCase().indexOf(keyword) == -1) continue;
						}
						
						result.files[file] = result.files[file]||[];
						result.files[file].push({
							'fullString':currentFile[row][col],
							'row':row,
							'col':col,
							'type':'cell'
						});
						result.count++;
					}
				}
			}
		}
	}
	
	if (options.searchLocations.includes("context")) {
		for (let idx in options.files) {
			let file = options.files[idx];
			if (Array.isArray(trans.project.files[file].context) == false) continue;
			let currentFile = trans.project.files[file].context;
			for (let row=0; row<currentFile.length; row++) {
				if (Array.isArray(currentFile[row]) == false) continue;
				if (currentFile[row].length == 0) continue;
				for (let cont=0; cont<currentFile[row].length; cont++) {
					if (typeof currentFile[row][cont] !== "string") continue;
					
					if (keywordExp) {
						if (keywordExp.test(currentFile[row][cont]) == false) continue;
					} else if (options.caseSensitive) {
						if (currentFile[row][cont].indexOf(keyword) == -1) continue;
					} else {
						if (currentFile[row][cont].toLowerCase().indexOf(keyword) == -1) continue;
					}
					
					result.files[file] = result.files[file]||[];
					result.files[file].push({
						'fullString':currentFile[row][cont],
						'row':row,
						'col':0,
						'type':'context'
					});
					result.count++;
				}
			}
		}
	}

	if (options.searchLocations.includes("tag")) {
		for (let cont in options.files) {
			let file = options.files[cont];
			let tags = keyword.split(" ");
			let currentFile = trans.project.files[file].data;
			for (let row=0; row<currentFile.length; row++) {
				if (trans.hasTags(tags, row, file) == false) continue;
				result.files[file] = result.files[file]||[];
				result.files[file].push({
					'fullString':currentFile[row][this.keyColumn],
					'row':row,
					'col':0,
					'type':'cell'
				});
				result.count++;
			}
		}
	}

	if (options.searchLocations.includes("comment")) {
		console.log("searching comment");
		for (let idx in options.files) {
			let file = options.files[idx];
			if (Boolean(trans.project.files[file].comments) == false) continue;
			let currentFile = trans.project.files[file].comments;
			console.log("processing", file);
			for (let row in currentFile) {
				if (Boolean(currentFile[row]) == false) continue;
				for (let col in currentFile[row]) {
					if (typeof currentFile[row][col] !== "string") continue;
					
					if (keywordExp) {
						if (keywordExp.test(currentFile[row][col]) == false) continue;
					} else if (options.caseSensitive) {
						if (currentFile[row][col].indexOf(keyword) == -1) continue;
					} else {
						if (currentFile[row][col].toLowerCase().indexOf(keyword) == -1) continue;
					}
					
					result.files[file] = result.files[file]||[];
					result.files[file].push({
						'fullString':currentFile[row][col],
						'row':row,
						'col':col,
						'type':'comment'
					});
					result.count++;
				}
			}
		}
	}

	var end = new Date().getTime();
	result.executionTime = end - start;
	return result;
}

/**
 * Search for some keyword and replace it with a text
 * @param {String} keyword - Keyword
 * @param {String} replacer - Replacer
 * @param {Object} options 
 * @param {Boolean} [options.caseSensitive] - Whether the search is in case sensitive mode or not
 * @param {Boolean} [options.lineMatch] - Whether the search is with the line match mode or not
 * @param {Boolean} [options.isRegexp] - Whether the keyword is a regexp or not
 * @returns {SearchResult} the search result
 */
 Trans.prototype.replace = function(keyword, replacer, options) {
	console.log("entering trans.search");

	if (typeof keyword == "undefined") return null;
	if (typeof keyword.length <=1) return t("Keyword too short!");
	if (typeof trans.project == "undefined") return null;
	if (typeof trans.project.files == "undefined") return null;
	
	replacer = replacer||"";
	options = options|| {};
	options.caseSensitive = options.caseSensitive||false;
	options.isRegexp = options.isRegexp||false;
	
	if (Array.isArray(options.files) == false) {
		options.files = [];
		for (var file in trans.project.files) {
			options.files.push(file);
		}
	}
	
	
	
	if (options.caseSensitive == false && options.isRegexp == false) {
		keyword = keyword.toLowerCase();
	}
	
	var start = new Date().getTime();
	var result = {
		'keyword':keyword,
		count:0,
		isRegexp:options.isRegexp,		
		executionTime:0,
		files:{}
	};
	
	// check if regexp is valid
	if (options.isRegexp) {
		var keywordExp = common.evalRegExpStr(keyword);
		if (keywordExp == false) {
			alert(keyword+t(" is not a valid javascript's regexp!\r\nFind out more about Javascipt's Regular Expression at :\r\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions"));
			return result;
		}
	}
	
	//for (var file in trans.project.files) {
	for (let cont in options.files) {
		let file = options.files[cont];
		if (Array.isArray(trans.project.files[file].data) == false) continue;
		let currentFile = trans.project.files[file].data;
		//console.log("handling file : ", file, currentFile);
		for (let row=0; row<currentFile.length; row++) {
			if (currentFile[row].length == 0) continue;
			for (let col=1; col<currentFile[row].length; col++) { // skip first row
				if (typeof currentFile[row][col] !== "string") continue;
				
				if (options.isRegexp) {
					// regular expression search
					if (keywordExp.test(currentFile[row][col])) {
						// match found
						let original = trans.project.files[file].data[row][col];

						trans.project.files[file].data[row][col] = currentFile[row][col].replace(keywordExp, replacer);
						result.files[file] = result.files[file]||[];
						result.files[file].push({
							'fullString':trans.project.files[file].data[row][col],
							'row':row,
							'col':col,
							'originalString' : original
							
						});
						result.count++;	
						
						break;							
					}
					continue;
				} 
				
				// normal search
				if (options.caseSensitive) {
					if (currentFile[row][col].indexOf(keyword) == -1) continue;
				} else {
					if (currentFile[row][col].toLowerCase().indexOf(keyword) == -1) continue;
				}
				
				let original = trans.project.files[file].data[row][col];
				trans.project.files[file].data[row][col] = currentFile[row][col].replaces(keyword, replacer, !options.caseSensitive);
				
				result.files[file] = result.files[file]||[];
				result.files[file].push({
					'fullString':trans.project.files[file].data[row][col],
					'row':row,
					'col':col,
					'originalString' : original
				});
				result.count++;
			}
		}
		
	}
	var end = new Date().getTime();
	result.executionTime = end - start;
	trans.refreshGrid();
	//trans.selectFile($(".fileList .selected"));
	
	return result;
	
}

/**
 * Search for some keyword and put the text on target column
 * @param {String} keyword - Keyword
 * @param {String} put - The text to put into
 * @param {Number} targetCOl - The index of the column to put into
 * @param {Object} options 
 * @param {Boolean} [options.caseSensitive] - Whether the search is in case sensitive mode or not
 * @param {Boolean} [options.lineMatch] - Whether the search is with the line match mode or not
 * @param {Boolean} [options.isRegexp] - Whether the keyword is a regexp or not
 * @returns {SearchResult} the search result
 */
 Trans.prototype.findPut = function(keyword, put, targetCol, options) {
	console.log("entering trans.search");

	if (typeof keyword == "undefined") return null;
	if (typeof keyword.length <=1) return t("Keyword too short!");
	if (typeof trans.project == "undefined") return null;
	if (typeof trans.project.files == "undefined") return null;
	if (targetCol < 1) return false;

	
	options = options|| {};
	options.caseSensitive 	= options.caseSensitive||false;
	options.lineMatch 		= options.lineMatch||false;
	
	if (typeof options.overwrite == "undefined") options.overwrite = true;
	
	if (Array.isArray(options.files) == false) {
		options.files = [];
		for (var file in trans.project.files) {
			options.files.push(file);
		}
	}
	
	if (options.caseSensitive == false) {
		keyword = keyword.toLowerCase();
	}
	
	var start = new Date().getTime();
	var result = {
		keyword	:keyword,
		count	:0,
		executionTime:0,
		files:{}
	};
	
	// todo: If keyword contains more than one line, then use row by row matching

	if (keyword.includes("\n") || options.mode=="rowByRow") {
		console.log("Entering row by row search");
		result = this.findAndInsert(keyword, put, targetCol , {
			insensitive : !options.caseSensitive,
			overwrite : options.overwrite
		});
	} else {
		//line match algorithm
		for (let cont in options.files) {
			let file = options.files[cont];
			if (Array.isArray(trans.project.files[file].data) == false) continue;
			let currentFile = trans.project.files[file].data;
			for (let row=0; row<currentFile.length; row++) {
				if (currentFile[row].length == 0) continue;
				for (let col=0; col<currentFile[row].length; col++) {
					if (typeof currentFile[row][col] !== "string") continue;
					
					if (options.caseSensitive) {
						if (currentFile[row][col].indexOf(keyword) == -1) continue;
					} else {
						if (currentFile[row][col].toLowerCase().indexOf(keyword) == -1) continue;
					}
					
					var lineIndex = common.lineIndex(currentFile[row][col], keyword, options.caseSensitive);
					if (lineIndex != -1) {
						// match found
						var newTxt = common.insertLineAt(currentFile[row][targetCol], put, lineIndex, {
								lineBreak:trans.project.files[file].lineBreak||"\n"
							});
						currentFile[row][targetCol] = newTxt;
						
						result.files[file] = result.files[file]||[];
						result.files[file].push({
							'fullString':currentFile[row][col],
							'row':row,
							'col':col,
							'type':'cell',
							'lineIndex':lineIndex
						});
						result.count++;	
						
						break;
					}
				}
			}
		}	
	}

	

	
	var end = new Date().getTime();
	result.executionTime = end - start;
	trans.refreshGrid();
	
	return result;
}

/**
 * Hooks
 */
class TransProjectHook{
	constructor () {
		this.hooks = {}
	}
}
TransProjectHook.prototype.defineHook = function(hookName, fn) {
	if (typeof hookName !== "string") throw new Error(`hookName must be a string ${typeof hookName} given`)
	if (typeof fn !== "function") throw new Error(`hookName must be a function ${typeof fn} given`)
	this.hooks[hookName] = fn;
}
TransProjectHook.prototype.getHook = function(hookName) {
	return this.hooks[hookName];
}

TransProjectHook.prototype.run = async function(hookName, ...args) {
	if (typeof this.hooks[hookName] == "function") {
		await this.hooks[hookName].apply(window.trans, args);
	}

	// if not defined then look for the setting in trans.project.options.hooks
    const hooks = trans.getOption("hooks");
    if (!hooks) return;
    if (typeof hooks?.afterExport !== "string") return;
	const userConsent = await ui.confirmRunScript();
	if (!userConsent) return;
	
    let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
    const newFunc = new AsyncFunction(hooks.afterExport);
	const execResult = newFunc.apply(trans, args);

	return execResult;
}



//================================================================
// 						NEW TRANS INSTANCE
//================================================================
/**
 * @namespace
 * @instance
 */
var trans = new Trans()
window.trans = trans;
trans.cellInfo = new Trans.CellInfo();

trans.projectHook = new TransProjectHook();
//trans.projectHook.hooks = require("www/js/trans.projecthooks.js");
/**
 * Grid context menu object
 * @memberof! Trans#
 */
 trans.gridContextMenu = {
	'commentsAddEdit': {
		name: t("Add comment"),
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return true;
			return false;
		}
	},
	'commentsRemove':{
		name: t("Delete comment"),
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return true;
			return false;
		}
		
	},
	"sepx": '---------',
	'translateThisCell':{
		name: function() {
			var def = "<span class='HOTMenuTranslateHere'>"+t("Translate here")+" <kbd>ctrl+g</kbd></span>";
			if (typeof trans.project == 'undefined') return def;
			trans.project.options = trans.project.options||{};
			var thisTrans = trans.getActiveTranslator()
			console.log("thisTrans", thisTrans);
			if (typeof thisTrans == 'undefined') return def;
			var from 	= trans.getSl()||"??";
			var to 		= trans.getTl()||"??";
			var thisTranslatorName = trans.getTranslatorEngine(thisTrans)?.name;
			if (!thisTranslatorName) return def;
			return t("Translate here using ")+thisTranslatorName+" ("+from+"<i class='icon-right-bold'></i>"+to+") <kbd>ctrl+g</kbd>"
		},
		callback: function() {
			trans.translateSelection();
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return true;
			if (!trans.getActiveTranslator()) return true;
			return false;
		}
		
	},
	'translateUsing': {
		name: function() {
			trans.drawGridTranslatorMenu();
			return t('Translate using...')
		},
		submenu: {
			items: []
		},
		hidden: function() {
			if (trans.grid.isRowHeaderSelected()) return true;
			return false;
		}
	},
	'mergeThenTranslate': {
		name: "<span>Merge then translate <kbd>ctrl+shift+g</kbd></span>",
		hidden: function() {
			if (trans.grid.isRowHeaderSelected()) return true;
			return false;
		},
		callback: async function() {
			trans.translateSelectionAsOneLine();
		}
	},
	'translateSimiliar': {
		name: "<span>Translate like this <kbd>ctrl+l</kbd></span>",
		hidden: function() {
			if (trans.grid.isRowHeaderSelected()) return true;
			return false;
		},
		callback: async function() {
			var conf = confirm(t("Do you want to translate all the same texts found across this project with the translation on the selected cell(s)?"))
			if (!conf) return;
			var result = await trans.translateAllBySelectedCells();
			alert(result.length + t(` cell(s) has been written!`))
		}
	},
	'---------':{},
	'columnWidth': {
		name: t("Column width"),
		callback: function(origin, selection, e) {
			console.log("column width : ", arguments);
			var cols = common.gridSelectedCols();
			var width = prompt("Enter new width", this.getColWidth(cols[0]));
			width = parseInt(width);
			
			if (width < 1) return alert(t("Width must greater than 0"))
			
			for (var i=0; i<cols.length; i++) {
				this.setColWidth(cols[i], width)	
			}						
		},
		hidden: function() {
			if (this.isColumnHeaderSelected()) return false;
			if (this.isRowHeaderSelected()) return true;
			return true;
		}

	},
	'sepn' :{
		name: '---------',
		hidden: function() {
			if (this.isColumnHeaderSelected()) return false;
			if (this.isRowHeaderSelected()) return true;
			return true;
		}					
	},

	'col-right': {
		name: t("Insert column right"),
		callback: function() {
			trans.grid.insertColumnRight();
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return false;
			if (trans.grid.isRowHeaderSelected()) return true;
			return true;
		}

		//disabled: false
	},
	'duplicateCol': {
		name: t("Duplicate column"),
		callback: function() {
			var getCol = trans.grid.getSelected()[0][1];
			var getColSet = trans.columns.length;
			var colHeaderName = trans.colHeaders[getCol]||"New Col";
			//var currentData = trans.grid.getData();
			trans.columns.push({});
			common.arrayExchange(trans.columns, getColSet, getCol + 1);
			common.arrayInsert(trans.colHeaders, getCol+1, colHeaderName);
			//batchArrayInsert(trans.data, getCol+1, null);
			console.log(trans.columns);
			trans.insertCell(getCol+1, null);
			trans.copyCol(getCol, getCol+1);

			trans.grid.updateSettings({
				colHeaders:trans.colHeaders
			})

			//trans.grid.render()
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected())  {
				if (trans.grid.getSelected()[0][1] != trans.grid.getSelected()[0][3] || 
				trans.grid.getSelected().length > 1 ) return true;
				return false;
			}
			
			return true;
		}

		//disabled: false
	},
	'removeColumn': {
		name: t("Remove this column"),
		callback: function(origin, selection, e) {
			console.log(arguments);
			var conf = confirm(t("Remove selected column?\nThis can not be undone!"));
			if (conf) {
				trans.removeColumn(selection[0].start.col, {refreshGrid:true});						
			}
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return false;
			if (trans.grid.isRowHeaderSelected()) return true;
			return true;
		}

	},
	'renameColumn': {
		name: t("Rename this column"),
		callback: function(origin, selection, e) {
			console.log(arguments);
			var thisCol = selection[0].start.col;
			var colName = trans.colHeaders[thisCol];
			var conf = prompt(t("Please enter new name"), colName);
			if (conf) {
				trans.renameColumn(thisCol, conf, {refreshGrid:true});						
			}
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return false;
			if (trans.grid.isRowHeaderSelected()) return true;
			return true;
		}
	},
	"sep0": '---------',

	'tags': {
		name:"Tags",
		submenu: {
			items: [
				{
					key: 'tags:red',
					name: '<i class="tag red icon-circle"></i> '+t('Red'),
					callback: function(key, selection, clickEvent) {
						trans.setTagForSelectedRow("red", selection);
					}
				},
				{
					key: 'tags:yellow',
					name: '<i class="tag yellow icon-circle"></i> '+t('Yellow'),
					callback: function(key, selection, clickEvent) {
						trans.setTagForSelectedRow("yellow", selection);
					}
				},
				{
					key: 'tags:green',
					name: '<i class="tag green icon-circle"></i> '+t('Green'),
					callback: function(key, selection, clickEvent) {
						trans.setTagForSelectedRow("green", selection);
					}
				},
				{
					key: 'tags:blue',
					name: '<i class="tag blue icon-circle"></i> '+t('Blue'),
					callback: function(key, selection, clickEvent) {
						trans.setTagForSelectedRow("blue", selection);
					}
				},
				{
					key: 'tags:gold',
					name: '<i class="tag gold icon-circle"></i> '+t('Gold'),
					callback: function(key, selection, clickEvent) {
						trans.setTagForSelectedRow("gold", selection);
					}
				},
				{
					key: 'tags:more',
					name: '<i class="tag icon-tags"></i> '+t('More tags...')+' <kbd>ctrl+t</kbd>',
					callback: function(key, selection, clickEvent) {
						//setTimeout(function() {
							ui.taggingDialog(selection);
						//}, 0);									
						
					}
				},
				{
					key: 'tags:clear',
					name: '<i class="tag icon-blank"></i> '+t('Clear tags'),
					callback: function(key, selection, clickEvent) {
						trans.clearTags(undefined, selection);
						trans.grid.render();
					}
				}
				
			]
		}
		
	},
	"sep1": '---------',
	'deleteRow': {
		name: function() {
			return t("Delete Row")+" <kbd>shift+del</kbd>"
		},
		callback: function(origin, selection, e) {
			var conf = confirm(t("Do you want to remove the currently selected row(s)?"));
			if (!conf) return;
			trans.removeRow(trans.getSelectedId(), common.gridSelectedRows());
			trans.refreshGrid();
			trans.grid.deselectCell();
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return false;
			return false;
		}
	},
	'clearContextTranslation': {
		name: t("Clear Context Translation")+" <kbd>alt+del</kbd>",
		callback: function(origin, selection, e) {
			
			/**
			 * Trigger when user runs Clear Context Translation 
			 * @event Trans#clearContextTranslationByRow
			 * @param  {Object} options
			 * @param  {Object} options.file
			 * @param  {Object} options.row
			 * @param  {Object} options.type
			 */
			trans.trigger("clearContextTranslationByRow", {file:trans.getSelectedId(), row:trans.grid.getSelectedRange(), type:"range"});
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return false;
			return false;
		}
	},				
	"sep2": '---------',
	'createAutomation' : {
		name: t("Create Automation"),
		callback: function(origin, selection, e) {
			console.log(arguments);
			var options = {
				workspace: "gridSelection",
				cellRange: trans.grid.getSelectedRange()
			}
			ui.openAutomationEditor("codeEditor_gridSelection", options);
		}
	},
	'runAutomation' : {
		name: ()=> {
			trans.updateRunScriptGridMenu();
			return t("Run Automation");
		}
	},	
	"sep3": '---------',
	'properties': {
		name: t("Row properties"),
		callback: function(origin, selection, e) {
			console.log(arguments);
			ui.openRowProperties();
		},
		hidden: function() {
			if (trans.grid.isColumnHeaderSelected()) return true;
			if (trans.grid.isRowHeaderSelected()) return false;
			return false;
		}
	}	
	/*
	,
	"colors": { // Own custom option
		name: 'Colors...',
		submenu: {
		  // Custom option with submenu of items
		  items: [
			{
			  // Key must be in the form "parent_key:child_key"
			  key: 'colors:red',
			  name: 'Red',
			  callback: function(key, selection, clickEvent) {
				setTimeout(function() {
				  alert('You clicked red!');
				}, 0);
			  }
			},
			{ key: 'colors:green', name: 'Green' },
			{ key: 'colors:blue', name: 'Blue' }
		  ]
		}
	},
	"credits": { // Own custom property
		// Custom rendered element in the context menu
		renderer: function(hot, wrapper, row, col, prop, itemValue) {
			console.log("rendering credits");
			console.log(arguments);
			var elem = document.createElement('marquee');
			elem.style.cssText = 'background: lightgray;';
			elem.textContent = 'Brought to you by...';
			return elem;
		},
		disableSelection: true, // Prevent mouseoever from highlighting the item for selection
		isCommand: false // Prevent clicks from executing command and closing the menu
	},
	"about": { // Own custom option
		name: function () { // `name` can be a string or a function
		  return '<b>Custom option</b>'; // Name can contain HTML
		},
		hidden: function () { // `hidden` can be a boolean or a function
		  // Hide the option when the first column was clicked

		  console.log(trans.grid.isColumnHeaderSelected());
		  if (trans.grid.isColumnHeaderSelected()) return false;
		  
		  //return this.getSelectedLast()[1] == 0; // `this` === hot3
		  return true;
		},
		callback: function(key, selection, clickEvent) { // Callback for specific option
		  setTimeout(function() {
			alert('Hello world!'); // Fire alert after menu close (with timeout)
		  }, 0);
		}
	}
	*/
}

 // backup current settings for close / new project actions
 //var transTemplate = JSON.parse(JSON.stringify(trans));
 
 trans.fileLoader = new FileLoader();
 trans.fileLoader.add("json", function(path) {
		trans.open(path);
 })
 trans.fileLoader.add("trans", function(path) {
		trans.open(path);
 })
 trans.fileLoader.add("tpp", function(path) {
	//trans.importTpp(openedFile);
	trans.importTpp(path);
})



/**
 * Attachment object.
 * Located at trans.project.attachments
 * @class
 * @param  {Object} obj
 */
window.Attachment = function(obj) {
	obj = obj || {};
	Object.assign(this, obj);
}


// ==============================================================
//
// 							E V E N T S
//
// ==============================================================


$(document).ready(function() {
	if ($('body').is('[data-window="trans"]') == false) return;

	trans.fileSelectorContextMenuInit();
	//trans.gridBodyContextMenu();
	
	const windowOnResize = function() {
		$(document).trigger("windowResizeStop");
		trans.grid.render();
		ui.fixCellInfoSize();
	}

	$(window).resize(debounce(windowOnResize, 100));
	


	trans.initTable();
	trans.isLoaded = true;

	// project level button initialization
	trans.on("transLoaded", async ()=> {
		console.warn("Trans loaded");
		$(".menu-button > .button-gridInfo.gridInfo").removeClass("checked")
		if (trans.getOption("gridInfo")?.isRuleActive) $(".menu-button > .button-gridInfo.gridInfo").addClass("checked")
	})
	
});