
/**************************************************************************
 *                     Translation tool
 *************************************************************************/

var REFRESH_PAGES_DELAY_SECS	= 60*30;
var REFRESH_FRAGS_DELAY_SECS	= 60*15;
// the edit refresh delay is defined from the lock duration minus 1 minute from the PHP



var TAB = String.fromCharCode(9);
var LF = String.fromCharCode(10);

var PATH_IMG			= '/images/trtool';
var IMG_ICO_SAVE		= PATH_IMG + '/save.gif';
var IMG_ICO_CONFIRM		= PATH_IMG + '/confirm.gif';
var IMG_ICO_CHECK		= PATH_IMG + '/check.gif';
var IMG_ICO_LOCKED		= PATH_IMG + '/locked.gif';
var IMG_ICO_UNLOCKED	= PATH_IMG + '/unlocked.gif';
var IMG_ICO_PAGE_FT		= PATH_IMG + '/page-status-fully-translated.gif';
var IMG_ICO_PAGE_NOT_FT	= PATH_IMG + '/page-status-not-fully-translated.gif';
var IMG_ICO_PAGE_NOT_FC	= PATH_IMG + '/page-status-not-fully-confirmed.gif';
var IMG_ICO_NAVIGATE	= PATH_IMG + '/navigate.gif';
var IMG_ICO_FRAG_NOT_T	= PATH_IMG + '/frag-not-translated.gif';
var IMG_ICO_FRAG_NOT_C	= PATH_IMG + '/frag-not-confirmed.gif';
var IMG_ICO_FRAG_OK		= PATH_IMG + '/frag-ok.gif';
var IMG_ICO_REFRESH		= PATH_IMG + '/refresh.gif';
var IMG_ICO_WARNING		= PATH_IMG + '/warning.gif';
var IMG_ICO_SEARCH		= PATH_IMG + '/search.gif';
var IMG_ICO_REVERT		= PATH_IMG + '/undo.gif';
var IMG_ICO_CLEAN		= PATH_IMG + '/clean.gif';
var IMG_ICO_CLEAR_CACHE	= PATH_IMG + '/clear-cache.gif';


var IMG_SHOW_PANEL		= PATH_IMG + '/show-panel.gif';
var IMG_BLANK			= PATH_IMG + '/blank.gif';

var HB_HEADER			= 'tool-title';
var HB_FOOTER			= 'tool-footer';
var HB_BOX_HEADER		= 'tool-box-title';
var HB_BOX_HELP			= 'help-title';

var EDITOR_SOURCE		= 1;
var EDITOR_GENERAL		= 2;
var EDITOR_DISTINCT		= 3;

var PERM_CODE_NINJA			= 'N';
var PERM_CODE_MODERATOR		= 'M';
var PERM_CODE_TRANSLATOR	= 'T';
var PERM_CODE_EVERYBODY		= '*';
var PERM_CODE_NOBODY		= '-';

var AR_LOAD_LANGUAGES			= 'loadLanguages';
var AR_LOAD_PAGES_INFOS			= 'loadPagesInfos';
var AR_LOAD_PAGE_FRAGMENTS		= 'loadPageFragments';
var AR_LOAD_FRAG_INFOS_AND_LOCK	= 'loadFragInfosAndLock';
var AR_SAVE_FRAG_INFOS			= 'saveFragInfos';
var AR_LOCK_PAGE				= 'lockPage';
var AR_CLEAN_PAGE				= 'cleanPage';
var AR_CLEAR_PAGE_CACHE			= 'clearPageCache';

var KEY_VERSION				= 'version';
var KEY_USER_ID				= 'user_id';
var KEY_IS_TRANSLATOR		= 'is_translator';
var KEY_IS_NINJA			= 'is_ninja';
var KEY_IS_ADMIN			= 'is_admin';
var KEY_LOCK_TIME			= 'lock_time';
var KEY_IS_DEV				= 'is_dev';


var KEY_ORIG_LANGUAGE_ABBRIV	= 'orig_lang_abbriv';
var KEY_LANGUAGE_ABBRIV			= 'language_abbriv';
var KEY_LANG_ABBRIV_SHORT		= 'lc';
var KEY_LANGUAGE_NAME			= 'language';
var KEY_LANGUAGE_ORIG_NAME		= 'local_language_name';
var KEY_NB_TRANSLATORS			= 'nb_transaltors';
var KEY_CAN_MODERATE_LANG		= 'is_moderator';
var KEY_LANGUAGE_ID				= 'language_id';
var KEY_LABEL					= 'lbl';
var KEY_ID						= 'id';
var KEY_LOCKED					= 'lk';
var KEY_LOCKED_FOR				= 'lkf';
var KEY_NB						= 'nb';
var KEY_NB_TRANSLATED			= 'nbt';
var KEY_NB_GEN					= 'nbg';
var KEY_NB_GEN_TRANSLATED		= 'nbgt';
var KEY_NB_UNCONFIRMED			= 'nbuc';
var KEY_MD5_HASH				= 'md5';
var KEY_LAST_REBUILD_AT			= 'lra';
var KEY_LAST_REBUILD_BY			= 'lrb';
var KEY_LOCKED_BY				= 'lby';

var KEY_FILE_ID				= 'wfid';
var KEY_PAGE_ID				= 'wpid';
var KEY_LOCATION_ID			= 'lid';
var KEY_FILE_PATH			= 'path';
var KEY_GENERAL_T			= 'gt';
var KEY_TEXT				= 't';
var KEY_CREATED_AT			= 'ct';
var KEY_TRANSLATED_BY		= 'tby';
var KEY_TRANSLATED_AT		= 'tat';
var KEY_TRANSLATED_DATE		= 'td';
var KEY_TRANSLATOR_PROFILE	= 'tp';
var KEY_CONFIRMED_BY		= 'cby';
var KEY_GEN_CONFIRMED_BY	= 'gcby';
var KEY_CONFIRMED_AT		= 'cat';
var KEY_CONFIRMED_DATE		= 'cd';
var KEY_CONFIRMER_PROFILE	= 'cp';
var KEY_LOCKER_PROFILE		= 'lp';
var KEY_FUNCTION_NAME		= 'fn';
var KEY_FILE_LABEL			= 'lf';


var INTEGER_KEYS = $A([KEY_USER_ID, KEY_CONFIRMED_AT, KEY_CONFIRMED_BY, KEY_CREATED_AT,
	KEY_FILE_ID, KEY_ID, KEY_LOCATION_ID, KEY_LOCKED_BY, KEY_LOCK_TIME, KEY_NB, KEY_NB_GEN,
	KEY_NB_GEN_TRANSLATED, KEY_NB_TRANSLATED, KEY_NB_UNCONFIRMED, KEY_PAGE_ID, KEY_TRANSLATED_AT,
	KEY_TRANSLATED_BY, KEY_GEN_CONFIRMED_BY]);

var TR_HELP = {
	WELCOME: {
		TITLE:	'Welcome !',
		BODY:	'Welcome to the new translation tool. This first version could appear to be with less functionalities than the previous one, but more than a new design and way to use, all the background system has been changed. And more and more functionalities will come quickly after some weeks of "testing" phase. So feel free to contact your translation team\'s coordinator if you have questions about the tool.'
	},
	LANGS: {
		TITLE:	'Languages panel',
		BODY:	'Select the language in which you want to translate to by clicking on it. Then click on the "Pages" panel to show the list of pages.'
	},
	PAGES: {
		TITLE:	'Pages panel',
		BODY:	'Select the page on which you want to work by clicking on it. Then click on the "Frags" panel to show the list of fragments present on the selected page.'
	},
	FRAGS: {
		TITLE:	'Fragments panel',
		BODY:	'Select the fragment on which you want to work by clicking on it. Then click on the "Edit" panel to translate or confirm one or more of its translation(s).'
	},
	EDIT: {
		TITLE:	'Edit panel',
		BODY:	'For almost all of the fragments, you only have to translate and confirm the "General translation". The "Local translations", which are only shown if you transalted the general one, are here if you need to have a diferent transaltion of the text on the selected page than the general one which is for all the pages where the same fragment could appear.'
	}
}


// extend the Array prototype to give access to a remove method
if ( !Array.prototype.remove ) {
	/**
	 * Remove the given element from the array
	 * @param from Integer Index of the first element to remove
	 * @param to Integer Index of the last element to remove, if not given, only [from]
	 * will be removed
	 */
	Array.prototype.remove = function(from, to) {
		var res = this.slice((to || from) + 1 || this.length);
		this.length = from < 0 ? this.length + from : from;
		return this.push.apply(this, res);
	};
}


/**
 * Generates a new ID for elements
 * @return String A unique element ID
 */
function tr_newUid() {
	if ( !tr_newUid.counter ) {
		tr_newUid.counter = 0;
	}
	return ('tr_' + (++tr_newUid.counter));
}

/**
 * Creates an HTML element extended with the prototype functions
 * @param name String Tag name of the element
 * @param attrs Array Attributes of the element
 * @param children Array The children of the element if any, each sub-element
 * being declared as an array of parameters given to this funciton
 * @return HTMLElement The created element and its children
 */
function _ce(name, attrs, children) {
	if ( name == '#' || name == 'T' ) return _ct(attrs);
	var e = $(document.createElement(name));
	var c;
	if ( attrs ) {
		$H(attrs).each(function (pair) {
			if ( pair.key == 'T' ) {
				e.appendChild(_ct(pair.value));
			} else if ( pair.key == 'style' ) {
				$H(pair.value).each(function (sp) {
					e.style[sp.key] = sp.value;
				});
			} else {
				e[pair.key] = pair.value;
			}
		});
		if ( name == 'img' && e.alt && e.alt != '' && !e.title ) {
			e.title = e.alt;
		}
		if ( e.title && e.title != '' ) {
			e.toolTip = new ToolTip(e);
			if ( e.title == '#' ) e.title = '';
		}
	}
	if ( children ) {
		$A(children).each(function (item) {
			if ( Object.isElement(item) ) {
				e.appendChild(item);
				return;
			}
			var n = item[0];
			var a = item.length > 1 ? item[1] : false;
			var c = item.length > 2 ? item[2] : false;
			e.appendChild(_ce(n, a, c));
		});
	}
	return e;
}

/**
 * Creates a text node
 * @param text String The text of the text node
 * @return HTMLTextELement The text node created
 */
function _ct(text) {
	return document.createTextNode(text);
}

/**
 * Creates a blank image element
 * @param width Integer The width of the image if needed
 * @param height Integer The height of the image
 * @return HTMLElement The img element
 */
function _bi(width, height) {
	var img = _ce('img', {src: IMG_BLANK, alt: '', title: '', className: 'tr-blank'});
	if ( width ) img.width = width;
	if ( height ) img.height = height;
	return img;
}

/**
 * Show the locker, avoiding the user to do any action on the page with help of a div
 * covering all the body
 * @param text String The text to display in the waiting message
 */
function showLocker(text) {
	var el = $('tr_locker');
	var msg = $('tr_message');
	var body = $(document.getElementsByTagName('body')[0]);
	var p, w, h;
	
	if ( !showLocker.stack ) {
		showLocker.stack = new Array();
	} else if ( !text ) {
		text = showLocker.stack.last();
	} else {
		text = text + ', please wait...';
	}
	if ( !text ) {
		text = 'Loading, please wait...';
	}
	showLocker.stack.push(text);
	if ( !el ) {
		el = _ce('iframe', {id: 'tr_locker'});
		body.insertBefore(el, body.firstChild);
		msg = _ce('div', {id: 'tr_message'});
		body.insertBefore(msg, body.firstChild);
	}
	msg.innerHTML = '<img src="/images/ajax-loader.gif" alt="Loading..." class="ajax-loader" />' + text + '&nbsp;<div class="tr-progress"><div></div></div>';
	showLocker.$progress = msg.lastChild;
	el.show();
	el.style.width = body.getWidth() + 'px';
	el.style.height = body.getHeight() + 'px';
	el.style.zIndex = 10000;
	msg.show();
	if ( TrTool.$root ) {
		p = TrTool.$root.cumulativeOffset();
		w = TrTool.$root.getWidth();
		h = TrTool.$root.getHeight();
	} else {
		p = body.cumulativeOffset();
		w = body.getWidth();
		h = body.getHeight();
	}
	msg.style.left = (p.left + Math.round((w - msg.getWidth()) / 2)) + 'px';
	msg.style.top = (p.top + Math.round((h - msg.getHeight()) / 2)) + 'px';
	msg.style.zIndex = 10001;
}
/**
 * Update the progress bar shown in the locker
 * @param value Number The current value
 * @param max Number The maximum value that can be [value]
 */
showLocker.showProgress = function(value, max) {
	if ( !showLocker.$progress ) return;
	var tw = showLocker.$progress.getWidth() - 4;
	var w = Math.round(value * tw / max);
	var p = Math.round(value * 100 / max);
	if ( w > tw ) w = tw;
	if ( p > 100 ) p = 100;
	showLocker.$progress.firstChild.update(p + ' %')
	showLocker.$progress.firstChild.style.width = w + 'px';
}

/**
 * Hide the previousely shown locker
 */
function hideLocker() {
	var el = $('tr_locker');
	var msg = $('tr_message');
	if ( showLocker.stack && showLocker.stack.length ) {
		showLocker.stack.pop();
		if ( showLocker.stack.length ) {
			msg.innerHTML = '<img src="/images/ajax-loader.gif" alt="Loading..." class="ajax-loader" />' + showLocker.stack.last() + '&nbsp;<div class="tr-progress"><div></div></div>';
			showLocker.$progress = msg.lastChild;
		} else {
			el.hide();
			msg.hide();
		}
	}
}

/**
 * Append an ajax request to the requests stack, showing the locker, executing callbacks
 * before and/or after, giving the language parameter if needed
 * @param action String The action name
 * @param params Object Additional parameters to give to the server
 * @param callback Object Object with maximum 2 properties: "before" defining a functon
 * to run before doing the request, and "after", a function to call after the request,
 * accepting one parameter, the response object
 * @param message String The message to show in the locker
 * @param debugMode Boolean Show debug informations
 */
function tr_ajax_request(action, params, callback, message, debugMode) {
	var mParams = Object.extend({
				action:				action,
				ajax:				'y',
				user_id:			TrTool.getUserId(),
				version:			TrTool.getVersion()
			}, params ? params : {});
	if ( TrTool.getLang() && !mParams.language_abrriv ) {
		mParams.language_abbriv = TrTool.getLang();
	}
	if ( tr_ajax_request.timerCount == 0 ) {
		tr_ajax_request.pe();
	}
	
	if ( debugMode && TrTool.isDev() ) alert($H(mParams).inspect());

	// prepare the function to add to the XMLHttpRequest stack
	var func = function () {
		callback && callback.before && callback.before();
		new Ajax.Request(window.location.href, {
			method:			'post',
			
			parameters: 	mParams,
			
			onSuccess: function (transport) {
				if ( !tr_ajax_request.$debug ) {
					tr_ajax_request.$debug = $(document.createElement('textarea'));
					document.getElementsByTagName('body')[0].appendChild(tr_ajax_request.$debug);
					tr_ajax_request.$debug.style.width = '100px';
					tr_ajax_request.$debug.style.height = '100px';
					tr_ajax_request.$debug.style.position = 'absolute';
					tr_ajax_request.$debug.style.top = '0px';
					tr_ajax_request.$debug.style.left = '0px';
					tr_ajax_request.$debug.onfocus = function () {
						tr_ajax_request.$debug.style.width = '1000px';
						tr_ajax_request.$debug.style.height = '600px';
					};
					tr_ajax_request.$debug.onblur = function () {
						tr_ajax_request.$debug.style.width = '100px';
						tr_ajax_request.$debug.style.height = '100px';
					};
				}

				tr_ajax_request.$debug.style.zIndex = 10000;
				if ( !TrTool.isDev() ) {
					tr_ajax_request.$debug.hide();
				}
				// execute smoothly the resulting code
				tr_ajax_request.smoothEval(transport.responseText, this, callback);
			},
			
			onException: function (transport, exception) {
				if ( TrTool.isDev() ) alert(exception);
				callback && callback.after && callback.after(false);
				tr_ajax_request.ev_endOfRequest();
				if ( TrTool.isDev() ) {
					throw exception;
				} else {
					alert('An error has occured on the page:\n' + exception)
				}
			},
			
			onFailure: function (transport) {
				alert('Sorry, an error occured while trying to save/load the data, try to reload the page!');
				callback && callback.after && callback.after(false);
				tr_ajax_request.ev_endOfRequest();
			}
		});
	};
	
	if ( TrTool.isReady() ) {
		func.lockerShown = true;
		showLocker(message);
	} else {
		func.lockerShown = false;
	}
	func.message = (message ? message : null);	
	tr_ajax_request.stack.unshift(func);
}
tr_ajax_request.stack = new Array();
tr_ajax_request.timerCount = 0;
tr_ajax_request.requestCount = 0;
/**
 * Periodical executer, running the requests presents in the stack
 */
tr_ajax_request.pe = function () {
	var f;
	tr_ajax_request.timerCount++;
	if ( TrTool && TrTool.isReady() && tr_ajax_request.stack.length ) {
		TrTool.setReady(false);
		f = tr_ajax_request.stack.pop();
		if ( !f.lockerShown ) {
			showLocker(f.message);
		}
		f();
		tr_ajax_request.requestCount++;
	}
	if ( TrTool.isDev() ) {
		window.defaultStatus = 'Request stack: ' + tr_ajax_request.stack.length + '  |  Request count: ' + tr_ajax_request.requestCount + '  |  Timer count: ' + tr_ajax_request.timerCount;
	}
	tr_ajax_request.timer = window.setTimeout(tr_ajax_request.pe, 200);
}
/**
 * Called at the totally end of a request, if no more requests in the stack
 */
tr_ajax_request.ev_endOfRequest = function () {
	hideLocker();
	TrTool.setReady(true);
}
tr_ajax_request.stmts = new Array();
tr_ajax_request.stmtsPos = 0;
tr_ajax_request.stmtsContext = null;
tr_ajax_request.stmtsCallback = null;
/**
 * Smoothly evaluate the JS code given, calling the callback then keeping the context
 * @param code String The code to execute
 * @param context Object The context in which to run the callback
 * @param callback Object The calback (see tr_ajax_request)
 */
tr_ajax_request.smoothEval = function(code, context, callback) {
	tr_ajax_request.stmts = code.strip().split(/\r\n|\n/);
	tr_ajax_request.stmtsPos = 0;
	tr_ajax_request.stmtsContext = context;
	tr_ajax_request.stmtsCallback = callback;
	tr_ajax_request.smoothEval_loop();
}
/**
 * Smooth execution loop
 */
tr_ajax_request.smoothEval_loop = function () {
	var code, i, l, bEnd = false, response;
	code = '';
	for ( i = 0; i < 10; i++ ) {
		l = tr_ajax_request.stmtsPos + i;
		if ( l >= tr_ajax_request.stmts.length ) {
			bEnd = true;
			break;
		}
		code += tr_ajax_request.stmts[l] + "\n";
	}
	tr_ajax_request.stmtsPos = l;
	eval(code);
	
	showLocker.showProgress(tr_ajax_request.stmtsPos, tr_ajax_request.stmts.length);
	if ( bEnd ) {
		tr_ajax_request.smoothEval_end.bind(tr_ajax_request.stmtsContext)(response);
	} else {
		window.setTimeout(tr_ajax_request.smoothEval_loop, 1);
	}
}
/**
 * End of a smooth execution
 */
tr_ajax_request.smoothEval_end = function (response) {
	var dbg;
	if ( TrTool.isDev() ) {
		dbg = '';
		if ( response && response.output ) {
			dbg += '----------- Output -------------' + LF + response.output + LF;
		}
		if ( response && response.dbqueries ) {
			dbg += '----------- DB queries -------------' + LF;
			$A(response.dbqueries).each(function (q) {
				dbg += q.get('sql') + LF;
				if ( q.get('error') ) {
					dbg += 'ERROR: ' + q.get('error') + LF;
				}
				dbg += LF;
			});
		}
		dbg += '----------- Response body -------------' + LF + tr_ajax_request.stmts.join("\n");
		tr_ajax_request.$debug.value = dbg;
		tr_ajax_request.$debug.show();
	}
	if ( response && response.error ) {
		alert(response.error);
	}
	if ( response && (!response.userId || response.userId != TrTool.getUserId()) ) {
		alert('Fatal synchronisation error with the server.' + LF + 'The page will be reloaded, sorry.');
		window.location.reload();
	}
	tr_ajax_request.stmts = new Array();
	tr_ajax_request.stmtsPos = 0;
	tr_ajax_request.stmtsContext = null;
	if ( response && !TrTool.checkVersion(response[KEY_VERSION] ? response[KEY_VERSION] : -1) ) {
		return;
	}
	tr_ajax_request.stmtsCallback && tr_ajax_request.stmtsCallback.after && tr_ajax_request.stmtsCallback.after(response);
	tr_ajax_request.stmtsCallback = null;
	tr_ajax_request.ev_endOfRequest();
}

/**
 * Handle an horizontal bar with boxes and background images with correct repetitions
 */
var TrHorizBar = Class.create({
	/**
	 * Constructor
	 * @param baseName String The base name of each image file
	 * @param numBoxes Integer The number of boxes to create (1 by default)
	 */
	initialize: function (baseName, numBoxes) {
		var i, tds;
		if ( !numBoxes ) var numBoxes = 1;
		
		this.$root = _ce('table', {
				className:		'tr-horiz-bar tr-' + baseName,
				cellSpacing:	0,
				cellPadding:	0,
				width:			'100%'
			}, [
				['tbody', {}, [
					['tr', {}, [
						['td', {className: 'tr-horiz-bar-side'}, [
							['img', {src: PATH_IMG + '/' + baseName + '-left.gif'}]
						]],
						['td', {className: 'tr-horiz-bar-side'}, [
							['img', {src: PATH_IMG + '/' + baseName + '-right.gif'}]
						]]
					]]
				]]
			]);
		tds = this.$root.getElementsByTagName('td');
		this.$left = tds[0];
		this.$right = tds[1];
		this.$cells = new Array();
		for ( i = 0; i < numBoxes; i++ ) {
			this.$cells[i] = _ce('td', {className: 'tr-horiz-bar-center'});
			this.$cells[i].style.backgroundImage = 'url(' + PATH_IMG + '/' + baseName + '-center.gif)';
			this.$cells[i].innerHTML = '&nbsp;';
			this.$right.parentNode.insertBefore(this.$cells[i], this.$right);
		}
	},

	/**
	 * Defines the text or node to put in the given cell
	 * @param i Integer The column's index
	 * @param textOrNode mixed The text to write in the cell, or the HTMLElement
	 * @return HTMLElement The created element in the cell
	 */
	setCell: function (i, textOrNode) {
		var node = this.$cells[i].update();
		if ( Object.isElement(textOrNode) ) {
			node.appendChild(textOrNode);
		} else {
			node.appendChild(_ct(textOrNode ? textOrNode : ''));
		}
		return node.lastChild;
	},

	/**
	 * Return the element corresponding to the given cell
	 * @param i Integer The cell's index
	 * @return HTMLElement The element corresponding to the cell
	 */
	getCell: function (i) {
		return this.$cells[i];
	},

	/**
	 * Defines the content of the main cell, ie the first one
	 * @param content mixed Text or element to display inside
	 * @êeturn HTMLElement The element created in the cell
	 */
	setContent: function (content) {
		return this.setCell(0, content);
	}
});

/**
 * Panel with title and content part, which can be expanded or collapsed
 */
var TrPanel = Class.create({
	/**
	 * Constructor
	 * @param title String The short title of the panel
	 * @param longTitle String The long titlte of the panel
	 */
	initialize: function(title, longTitle) {
		var lp = TrPanel.$panels.last();
		if ( !lp ) lp = null;
		// collapsed
		this.$cTitle = _ce('div', {className: 'tr-title', T: title});
		this.$cBtnShow = _ce('img', {src: IMG_SHOW_PANEL, alt: 'Show', title: 'Show'});
		this.$collapsed = _ce('div', {className: 'tr-panel-collapsed', title: 'Show'},
			[this.$cTitle, this.$cBtnShow]);
		
		// expanded
		this.eTitle = new TrHorizBar(HB_BOX_HEADER, 2);
		this.eTitle.setCell(0, longTitle);
		this.$title = this.eTitle.getCell(0);
		this.$title.style.textAlign = 'left';
		this.$infos = this.eTitle.getCell(1);
		this.$infos.addClassName('tr-infos');
		this.$eTitle = _ce('div', {className: 'tr-title'}, [this.eTitle.$root]);
		this.$eContent = _ce('div', {className: 'tr-content'});
		this.$expanded = _ce('div', {className: 'tr-panel-expanded'}, [this.$eTitle, this.$eContent]);
		
		// root element
		this.$root = _ce('td', {className: 'tr-panel tr-panel-collapsed tr-pointer'}, [this.$collapsed, this.$expanded]);
		
		this.collapsed = true;
		this.disabled = false;
		this.nextPanel = null;
		this.previousPanel = lp;
		if ( lp ) lp.nextPanel = this;
		TrPanel.$panels.push(this);
		TrPanel.$container.appendChild(this.$root);
		
		// events and stuff to do once created
		this.$expanded.hide();
		this.$root.onclick = this.ev_clickOnCollapsed.bindAsEventListener(this);
	},
	
	/**
	 * Defines the long title of the panel
	 * @param text String The title
	 */
	setTitle: function (text) {
		this.$title.update().appendChild(Object.isElement(text) ? text : _ct(text));
	},
	
	/**
	 * Return the element of the title box
	 * @return HTMLElement The title element
	 */
	getTitleBox: function () {
		return this.eTitle.getCell(1);
	},
	
	/**
	 * Disable the panel
	 * @param yes Boolean Set to tru to disable, else false
	 */
	disable: function (yes) {
		var b = (yes == undefined ? true : !!yes);
		var res = true;
		if ( !this.disabled && b && this.expanded ) {
			res = this.collapse();
		}
		this.disabled = b;
		b = (b ? 'add' : 'remove') + 'ClassName';
		this.$root[b]('tr-disabled');
		this.$root[b]('tr-forbidden-pointer');
		return res;
	},
	
	/**
	 * Finds weither the panel is expanded or not
	 * @return Boolean Returns true if the panel is expanded, else false
	 */
	isExpanded: function () {
		return !this.collapsed;
	},
	
	/**
	 * Finds weither the panel is disabled or not
	 * @return Boolean Returns true if disabled, else false
	 */
	isDisabled: function () {
		return !!this.disabled;
	},
		
	/**
	 * Collapse the panel
	 * @param nextPanel TrPanel The anel to be expanded at its place
	 * @return Boolean Returns true if the action has been made, else false
	 */
	collapse: function (nextPanel) {
		if ( this.collapsed ) {
			return true;
		}
		if ( this.oncollapse && !this.oncollapse(this) ) {
			return false;
		}
		this.$expanded.hide();
		this.$collapsed.show();
		this.$root.removeClassName('tr-panel-expanded');
		this.$root.addClassName('tr-panel-collapsed');
		this.$root.addClassName('tr-pointer');
		this.$root.onclick = this.ev_clickOnCollapsed.bindAsEventListener(this);
		this.collapsed = true;
		TrPanel.selected = null;
		return true;
	},
	
	/**
	 * Expand the panel
	 * @return Boolean Returns true if the action has been made, else false
	 */
	expand: function () {
		if ( !this.collapsed ) {
			return true;
		}
		if ( this.onexpand && !this.onexpand(TrPanel.selected) ) {
			return false;
		}
		if ( TrPanel.selected && !TrPanel.selected.collapse(this) ) {
			return false;
		}
		this.$collapsed.hide();
		this.$expanded.show();
		this.$root.removeClassName('tr-panel-collapsed');
		this.$root.removeClassName('tr-pointer');
		this.$root.addClassName('tr-panel-expanded');
		this.$root.onclick = function() {};
		this.collapsed = false;
		TrPanel.selected = this;
		TrTool.updateTitle();
		TrPanel.updateDisabled();
		return true;
	},
	
	/**
	 * Clear the content of the panel body
	 */
	clearContent: function () {
		while ( this.$eContent.firstChild ) {
			this.$eContent.removeChild(this.$eContent.firstChild);
		}
	},
	
	/**
	 * Define the element that has to be the content of the panel body
	 * @param elem HTMLElement THe element to insert into the panel's body
	 */
	setContent: function (elem) {
		this.clearContent();
		this.$eContent.appendChild(elem);
	},
	
	/**
	 * Unselect anything in the list if the panel has a list inside
	 */
	clearSelection: function () {
		var s;
		if ( this.list && this.list.selected ) {
			s = this.list.selected;
			s.unselect();
		}
	},
	
	
	// events =============================================================
	ev_clickOnCollapsed: function (e) {
		if ( !this.isDisabled() ) {
			this.expand();
		}
	}
});
/**
 * Define the container of the panels container
 * @param el HTMLElement The element in which to insert the panel container
 */
TrPanel.setContainer = function (el) {
	if ( TrPanel.$panels ) return false;
	TrPanel.$container = _ce('tr');
	TrPanel.$root = _ce('table', {
			className:		'tr-panels',
			cellSpacing:	0,
			cellPadding:	0
		}, [
			['tbody', {}, [TrPanel.$container]]
		]);
	el.appendChild(TrPanel.$root);
	TrPanel.$panels = new Array();
	return true;
}
TrPanel.selected = null;
/**
 * Disabled all panels which are after the next one of the currently open one
 */
TrPanel.updateDisabled = function () {
	var d = -1;
	this.$panels.each(function (p) {
		if ( d == -1 && p == this.selected ) {
			d = 0;
			p.disable(false);
			
		} else if ( d == 0 ) {
			d++;
			p.disable(false);
			
		} else if ( d > 0 ) {
			p.disable();
		}
	}, this);
}


/**
 * Language panel
 */
var TrLanguagesPanel = Class.create(TrPanel, {
	/**
	 * Constructor
	 * @see TrPanel.initialize
	 */
	initialize: function ($super) {
		$super('Langs', 'Languages');
		this.list = new TrLanguageList(this);
		this.setContent(this.list.$root);
		// search box
		this.$searchText = _ce('input');
		this.$searchBtn = _ce('img', {src: IMG_ICO_SEARCH, className: 'tr-pointer', alt: 'Filter'});
		this.$search = _ce('div', {className: 'tr-search tr-language'}, [this.$searchText, this.$searchBtn]);
		this.getTitleBox().appendChild(this.$search);
		
		// events
		this.$searchBtn.onclick = this.ev_clickOnSearch.bindAsEventListener(this);
		this.$searchText.onkeyup = this.ev_keyupOnSearchText.bindAsEventListener(this);
	},
	
	/**
	 * Executed when the panel is collapsed
	 * @param nextPanel TrPanel The panel which will be expanded then
	 * @return Boolean Returns true if the panel can be collapsed or false if not
	 */
	oncollapse: function (nextPanel) {
		return true;
	},

	/**
	 * Executed when the panel is expanded
	 * @param previousPanel TrPanel The previousely expanded panel
	 * @return Boolean Returns true if the panel can be expanded, else false
	 */
	onexpand: function (previousPanel) {
		if ( this.list.loadAll() ) {
			if ( this.list.selected && this.list.selected.onselect ) {
				this.list.selected.onselect();
			} else {
				TrTool.clearHelp();
				TrTool.createHelp(TR_HELP.LANGS.TITLE, TR_HELP.LANGS.BODY);
			}
			return true;
		}
		return false;
	},
	
	/**
	 * Returns the selected language list item selected or its data
	 * @param listItem Boolean If true, the list item object will be returned, else its data
	 * @return mixed List item or its data of the selected language, ese null if no selection
	 */
	getSelectedLanguage: function (listItem) {
		return (this.list && this.list.selected) ? (listItem ? this.list.selected : this.list.selected.data) : null;
	},
	
	/**
	 * Get the language data or list item looking at the given abbriv
	 * @param abbriv String The language abbriv
	 * @param listItem Boolean If true, the list item will be returned, else its data
	 * @return mixed, Either list item of the language or its data
	 */
	getLanguageByAbbriv: function (abbriv, listItem) {
		var res = this.list ? this.list.getItemByKeyValue(KEY_LANGUAGE_ABBRIV, abbriv) : null;
		return res ? (listItem ? res : res.data) : null; 
	},
	
	/**
	 * Append a language
	 * @param lang Object Details of the language to add
	 * @return TrLanguageListItem The added item
	 */
	appendLanguage: function (lang) {
		var l = new TrLanguageListItem(lang);
		this.list.append(l);
		return l;
	},
	
	/**
	 * Select a language looking at the given abbriv
	 * @param abbriv String The abbriv of the langauge to select
	 * @return Boolean Returns true if te language has been selected, else false
	 */
	selectLanguage: function (abbriv) {
		var res = this.getLanguageByAbbriv(abbriv, true);
		return (res && res.isSelectable()) ? res.select() : false;
	},
	
	/**
	 * Apply the current selected filters
	 */
	applySearch: function () {
		var f, st = this.$searchText.value || '';
		st = st.strip().toLowerCase();
		if ( st == '' ) {
			f = function (item) { return true; };
		} else {
			f = function (item) {
				return (item.getData(KEY_LANGUAGE_NAME, '').toLowerCase().indexOf(st) >= 0);
			}
		}
		this.list.filter(f);
	},
	
	
	// events =========================================
	ev_clickOnSearch: function (e) {
		this.applySearch();
	},
	
	
	ev_keyupOnSearchText: function (e) {
		if ( e.keyCode == Event.KEY_RETURN ) {
			this.applySearch();
		}
	}
});


/**
 * Pages panel
 */
var TrPagesPanel = Class.create(TrPanel, {
	/**
	 * Constructor
	 */
	initialize: function ($super) {
		var f;
		$super('Pages', 'Pages');
		this.list = new TrPageList(this);
		this.setContent(this.list.$root);
		// search box
		this.$refreshBtn = _ce('img', {src: IMG_ICO_REFRESH, className: 'tr-pointer', alt: 'Refresh the list'});

		this.$searchText = _ce('input');
		
		this.$searchTr = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchTrLbl = _ce('label', {htmlFor: this.$searchTr.id}, [['img', {src: IMG_ICO_PAGE_FT, alt: 'Fully translated (includes the page(s) where ALL fragments have been translated and confirmed)'}]]);
		
		this.$searchNc = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchNcLbl = _ce('label', {htmlFor: this.$searchNc.id}, [['img', {src: IMG_ICO_PAGE_NOT_FC, alt: 'Need confirmation(s) (includes the page(s) where one or more fragments are translated but not confirmed)'}]]);
		
		this.$searchNt = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchNtLbl = _ce('label', {htmlFor: this.$searchNt.id}, [['img', {src: IMG_ICO_PAGE_NOT_FT, alt: 'Need translation(s) (includes the page(s) where one or more fragments need a translation)'}]]);
		
		this.$searchBtn = _ce('img', {src: IMG_ICO_SEARCH, className: 'tr-pointer', alt: 'Filter'});
		this.$search = _ce('div', {className: 'tr-search tr-page'}, [
			this.$refreshBtn, this.$searchTr, this.$searchTrLbl, this.$searchNc, this.$searchNcLbl, this.$searchNt, this.$searchNtLbl,
			this.$searchText, this.$searchBtn
		]);
		this.getTitleBox().appendChild(this.$search);
		this.$searchNc.checked = true;
		this.$searchNt.checked = true;
		
		// events
		this.$searchBtn.onclick = this.ev_clickOnSearch.bindAsEventListener(this);
		this.$searchText.onkeyup = this.ev_keyupOnSearchText.bindAsEventListener(this);
		f = (function () {
			this.applySearch();
		}).bindAsEventListener(this);
		this.$searchTr.onchange = f;
		this.$searchNc.onchange = f;
		this.$searchNt.onchange = f;
		this.$refreshBtn.onclick = this.ev_clickOnRefresh.bindAsEventListener(this);
	},

	/**
	 * Executed when the panel is collapsed
	 * @param nextPanel TrPanel The panel to be expanded then
	 * @return Boolean Returns true if the panel can be collapsed, else false
	 */
	oncollapse: function (nextPanel) {
		return true;
	},

	/**
	 * Executed when the panel is expanded
	 * @param prevPanel TrPanel The panel which has been collapsed
	 * @return Boolean Returns true if the panel can be expanded, else false
	 */
	onexpand: function (prevPanel) {
		if ( !TrTool.pLangs.list.selected || !TrTool.pLangs.list.selected.isSelectable() ) {
			alert('Please, select a language first.');
			return false;
		}
		if ( this.list.loadInfos() ) {
			if ( this.list.selected && this.list.selected.onselect ) {
				this.list.selected.onselect();
			} else {
				TrTool.clearHelp();
				TrTool.createHelp(TR_HELP.PAGES.TITLE, TR_HELP.PAGES.BODY);
			}
			return true;
		}
		return false;
	},
	
	/**
	 * Append a page
	 * @param data Object The page's data
	 * @return TrPageListItem The added item in the list
	 */
	appendPage: function (data) {
		var p = new TrPageListItem(data);
		this.list.append(p);
		return p;
	},
	
	/**
	 * Returns the selected page
	 * @param listItem Boolean True to return the list item, false to have its data
	 * @return mixed Selected page's data or list item
	 */
	getSelectedPage: function (listItem) {
		return (this.list && this.list.selected) ? (listItem ? this.list.selected : this.list.selected.data) : null;
	},
	
	/**
	 * Get a page looking at its id
	 * @param id Integer The page's ID
	 * @param listItem True to return the list item, false to return its data
	 * @return mixed The data of the page or its list item
	 */
	getPageById : function (id, listItem) {
		var res = this.list ? this.list.getItemByKeyValue(KEY_ID, id) : null;
		return res ? (listItem ? res : res.data) : null;
	},
	
	/**
	 * Select a page looking at the given ID
	 * @param id Integer The ID of the page to select
	 * @return Boolean Returns true if the page has beenselected, else false
	 */
	selectPage: function (id) {
		var p = this.list ? this.getPageById(id, true) : null;
		if ( p && p.isSelectable() ) return p.select();
		return false;
	},
	
	/**
	 * Update the page's data
	 * @param data Object New values of the page
	 * @return Boolean
	 */
	updatePageData: function (data) {
		var p = this.list ? this.getPageById(data.get(KEY_ID), true) : null;
		if ( !p ) return false;
		return p.updateData(data);
	},
	
	/**
	 * Reset all page's data
	 */
	resetPageDatas: function () {
		if ( !this.list ) return;
		this.list.items.each(function (item) {
			item.resetData(KEY_NB, KEY_NB_TRANSLATED, KEY_NB_GEN, KEY_NB_GEN_TRANSLATED, KEY_NB_UNCONFIRMED, KEY_LOCKED);
			item.afterDataUpdate();
		});
	},

	/**
	 * Apply search filters
	 */
	applySearch: function () {
		var f = new Array(), st = this.$searchText.value || '', me = this;
		st = st.strip().toLowerCase();
		f = function (item) {
			var ok = false, ft, nc, nt;
			if ( !item.isVisible() ) {
				return false;
			}
			ft = !!(item.isFullyTranslated() && me.$searchTr.checked);
			nc = !!(item.needConfirmation() && me.$searchNc.checked);
			nt = !!(item.needTranslation() && me.$searchNt.checked);
			ok = ft || nc || nt;
			if ( ok && st != '' ) {
				ok = (item.getData(KEY_LABEL, '').toLowerCase().indexOf(st) >= 0);
			}
			return ok;
		};
		this.list.filter(f);
	},
	
	
	// events =========================================
	ev_clickOnSearch: function (e) {
		this.applySearch();
	},
	
	
	ev_keyupOnSearchText: function (e) {
		if ( e.keyCode == Event.KEY_RETURN ) {
			this.applySearch();
		}
	},
	
	
	ev_clickOnRefresh: function (e) {
		this.list.loadInfos(true);
	}
});


/**
 * Fragments panel
 */
var TrFragmentsPanel = Class.create(TrPanel, {
	/**
	 * Constructor
	 */
	initialize: function ($super) {
		$super('Frags', 'Fragments');
		var f;
		this.translatedFragsLoaded = false;
		this.list = new TrFragmentList(this);
		this.setContent(this.list.$root);
		// search box
		this.$searchText = _ce('input');
		
		this.$searchTr = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchTrLbl = _ce('label', {htmlFor: this.$searchTr.id}, [['img', {src: IMG_ICO_PAGE_FT, alt: 'Translated (includes the fragment(s) which are translated and confirmed)'}]]);
		
		this.$searchNc = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchNcLbl = _ce('label', {htmlFor: this.$searchNc.id}, [['img', {src: IMG_ICO_PAGE_NOT_FC, alt: 'Need confirmation(s) (includes the fragment(s) which are translated but not confirmed)'}]]);
		
		this.$searchNt = _ce('input', {type: 'checkbox', id: tr_newUid(), className: 'tr-checkbox'});
		this.$searchNtLbl = _ce('label', {htmlFor: this.$searchNt.id}, [['img', {src: IMG_ICO_PAGE_NOT_FT, alt: 'Not translated (includes the fragment(s) which are not translated)'}]]);
		
		this.$searchBtn = _ce('img', {src: IMG_ICO_SEARCH, className: 'tr-pointer', alt: 'Filter'});
		this.$search = _ce('div', {className: 'tr-search tr-fragment'}, [
			this.$searchTr, this.$searchTrLbl, this.$searchNc, this.$searchNcLbl, this.$searchNt, this.$searchNtLbl,
			this.$searchText, this.$searchBtn
		]);
		this.getTitleBox().appendChild(this.$search);
		this.$searchNc.checked = true;
		this.$searchNt.checked = true;
		
		// events
		this.$searchBtn.onclick = this.ev_clickOnSearch.bindAsEventListener(this);
		this.$searchText.onkeyup = this.ev_keyupOnSearchText.bindAsEventListener(this);
		f = (function () {
			this.applySearch();
		}).bindAsEventListener(this);
		this.$searchTr.onchange = f
		this.$searchNc.onchange = f;
		this.$searchNt.onchange = f;
	},
	
	/**
	 * Executed when the panel is collapsed
	 * @param nextPanel TrPanel The panel to be expanded then
	 * @return Returns true if the panel can be collapsed, else false
	 */
	oncollapse: function (nextPanel) {
		return true;
	},

	/**
	 * Executed when the panel is expanded
	 * @param prevPanel TrPanel The panel which has been collapsed
	 * @return Returns true if the panel can be expanded, else false
	 */
	onexpand: function (prevPanel) {
		if ( TrTool.pLangs == prevPanel ) {
			alert('You must go through the "Pages" panel first.');
			return false;
		}
		if ( !TrTool.pPages.list.selected || !TrTool.pPages.list.selected.isSelectable() ) {
			alert('Please, select a page first.');
			return false;
		}
		if ( this.list.loadAll(TrTool.getPageId()) ) {
			if ( this.list.selected && this.list.selected.onselect ) {
				this.list.selected.onselect();
			} else {
				TrTool.clearHelp();
				TrTool.createHelp(TR_HELP.FRAGS.TITLE, TR_HELP.FRAGS.BODY);
			}
			return true;
		}
		return false;
	},
	
	/**
	 * Append a fragment
	 * @param data Object The fragment's data
	 * @return TrFragmentListItem The list item created
	 */
	appendFragment: function (data) {
		var f = new TrFragmentListItem(data);
		this.list.append(f);
		return f;
	},
	
	/**
	 * Update a fragment's data
	 * @param data Object The new data of a fragment
	 * @return Boolean
	 */
	updateFragmentData: function (data) {
		var f = this.list ? this.getFragmentByMd5(data.get(KEY_MD5_HASH), true) : null;
		if ( !f ) return false;
		return f.updateData(data);
	},
	
	/**
	 * Get the slected fragment list item or its data
	 * @param listItem Boolean True to return the list item, false for its data
	 * @return mixed The selected fragment's list item or its data
	 */
	getSelectedFragment: function (listItem) {
		return (this.list && this.list.selected) ? (listItem ? this.list.selected : this.list.selected.data) : null;
	},
	
	/**
	 * Get a fragment's list item or data looking at the given MD5 hash
	 * @param md5 String The fragment's hash
	 * @param listItem Boolean True to get the list item, false for its data
	 * @return mixed The fragment's list item or its data
	 */
	getFragmentByMd5 : function (md5, listItem) {
		var res = this.list ? this.list.getItemByKeyValue(KEY_MD5_HASH, md5) : null;
		return res ? (listItem ? res : res.data) : null;
	},
	
	/**
	 * Select a fragment looking at the given hash
	 * @param md5 String THe fragment's hash to select
	 * @return Boolean Returns true if the fragment has been selected, else false
	 */
	selectFragment: function (md5) {
		var f = this.list ? this.getFragmentByMd5(md5, true) : null;
		if ( f && f.isSelectable() ) return f.select();
		return false;
	},
	
	/**
	 * Remove all fragments from the list
	 * @return Boolean
	 */
	removeAllFragments: function () {
		if ( !this.list ) return;
		this.list.removeAll();
	},

	/**
	 * Apply the current filters
	 * @return Boolean
	 */
	applySearch: function () {
		var f = new Array(), st = this.$searchText.value || '', me = this;
		if ( this.$searchTr.checked && !this.translatedFragsLoaded )
		{
			if ( this.list.loadAll(TrTool.getPageId(), true) ) {
				if ( this.list.selected && this.list.selected.onselect ) {
					this.list.selected.onselect();
				}
			}
			return;
		}
		st = st.strip().toLowerCase();
		f = function (item) {
			var ok = false, ft, nc, nt;
			ft = !!(item.isTranslated() && me.$searchTr.checked);
			nc = !!(item.needConfirmation() && me.$searchNc.checked);
			nt = !!(item.needTranslation() && me.$searchNt.checked);
			ok = ft || nc || nt;
			if ( ok && st != '' ) {
				ok = (item.getData(KEY_TEXT, '').toLowerCase().indexOf(st) >= 0);
				ok = ok || (st.length == 32 && item.getData(KEY_MD5_HASH, '') == st);
			}
			return ok;
		};
		this.list.filter(f);
	},
	
	
	// events =========================================
	ev_clickOnSearch: function (e) {
		this.applySearch();
	},
	
	
	ev_keyupOnSearchText: function (e) {
		if ( e.keyCode == Event.KEY_RETURN ) {
			this.applySearch();
		}
	}
});


/**
 * Edit panel
 */
var TrEditPanel = Class.create(TrPanel, {
	initialize: function ($super) {
		$super('Edit', 'Edit fragment');
		this.editor = new TrFragmentEditor(this);
		this.setContent(this.editor.getElement());
	},
	
	
	oncollapse: function (nextPanel) {
		if ( this.editor.changed() ) {
			if ( !confirm('You didn\'t save your modification(s), it\'ll be lost if you continue.' + LF + 'Do you want to leave the editor panel anyway?') ) {
				return false;
			}
			this.editor.revert();
		}
		return true;
	},


	onexpand: function (prevPanel) {
		if ( TrTool.pLangs == prevPanel ) {
			alert('You must go through the "Pages" panel first.');
			return false;
		}
		if ( TrTool.pPages == prevPanel ) {
			alert('You must go through the "Frags" panel first.');
			return false;
		}
		if ( !TrTool.pFrags.list.selected || !TrTool.pFrags.list.selected.isSelectable() ) {
			alert('Please, select a fragment first.');
			return false;
		}
		if ( this.editor.load(TrTool.getPageId(), TrTool.getFragmentMd5Hash()) ) {
			TrTool.clearHelp();
			TrTool.createHelp(TR_HELP.EDIT.TITLE, TR_HELP.EDIT.BODY);
			return true;
		}
		return false;
	}
});


/**
 * Admin panel
 */
var TrAdminPanel = Class.create(TrPanel, {
	initialize: function ($super) {
		$super('Admin', 'Administration');
		this.disable();
	}
});



var TrHelpBox = Class.create({
	initialize: function (title, body) {
		this.title = new TrHorizBar(HB_BOX_HELP);
		this.$body = _ce('div', {className: 'tr-content'});
		this.$root = _ce('div', {className: 'tr-help-box'}, [this.title.$root, this.$body]);
		this.setContent(body);
		this.setTitle(title);
	},
	
	
	getElement: function () {
		return this.$root;
	},
	
	
	setContent: function (content) {
		var el;
		if ( Object.isElement(content) ) {
			el = content;
		} else {
			el = _ct(content);
		}
		this.$body.update().appendChild(el);
	},
	
	
	getContent: function () {
		return this.$body;
	},
	
	
	setHtmlContent: function (html) {
		this.$body.update().innerHTML = html;
	},
	
	
	setRightAlign: function (yes) {
		this.$body.style.textAlign = (yes == undefined ? true : !!yes) ? 'right' : '';
	},
	
	
	setTitle: function (title) {
		this.title.setContent(title);
	},
	
	
	getTitle: function () {
		return this.title.getCell(0);
	},
	
	
	show: function (yes) {
		this.$root[(yes == undefined || !yes) ? 'show' : 'hide']();
	}
});



/**
 * Pretty HTML tooltip
 */
var ToolTip = Class.create({
	initialize: function (elem, text) {
		this.setContent(text);
		this.$elem = elem;
		this.$elem.observe('mousemove', this.ev_mousemove.bind(this));
		this.$elem.observe('mouseout', this.ev_mouseout.bind(this));
		this.oldTitle = null;
	},
	
	
	setContent: function (content) {
		this.content = content ? content : null;
		return this.content;
	},
	
	
	setHtmlContent: function (html) {
		var e = _ce('span');
		e.innerHTML = html;
		this.content = e;
	},
	
	
	show: function (e) {
		if ( this.oldTitle && this.$elem.title == '' ) {
			this.$elem.title = this.oldTitle;
		}
		this.oldTitle = null;
		if ( !this.content && this.$elem.title ) {
			this.oldTitle = this.$elem.title;
			this.$elem.title = '';
			ToolTip.update(this.oldTitle, e);
		} else {
			if ( !this.content ) return;
			this.oldTitle = null;
			ToolTip.update(this.content, e);
		}
	},
	
	
	
	
	// events ==================================================================
	ev_mousemove: function (e) {
		this.show(e);
	},
	
	
	ev_mouseout: function (e) {
		ToolTip.hide();
		if ( this.oldTitle && this.$elem.title == '' ) {
			this.$elem.title = this.oldTitle;
		}
		this.oldTitle = null;
	}
});
ToolTip.show = function () {
	if ( !this.$root ) {
		this.$root = _ce('div', {className: 'tr-tooltip'});
		this.$root.hide();
		document.getElementsByTagName('body')[0].appendChild(this.$root);
	}
	this.$root.show();
	this.$root.style.zIndex = 10001
}
ToolTip.hide = function () {
	this.$root && this.$root.hide();
}
ToolTip.positionate = function (e) {
	var mx, my, w, h, x, y, vw, vh, vx, vy;
	mx = e.pointerX();
	my = e.pointerY();
	this.$root.style.width = '';
	this.$root.style.fontSize = '0.9em';
	w = this.$root.getWidth();
	h = this.$root.getHeight();
	if ( w > 350 ) {
		w = 350;
		this.$root.style.width = w + 'px';
		h = this.$root.getHeight();
	}
	if ( h > 200 ) {
		this.$root.style.fontSize = '0.8em';
		h = this.$root.getHeight();
	}
	vx = document.viewport.getScrollOffsets();
	vy = vx.top;
	vx = vx.left;
	vw = document.viewport.getWidth();
	vh = document.viewport.getHeight();
	x = Math.round(mx - (w / 2)) + 5;
	y = my + 25
	if ( x + w >= vx + vw - 10 ) {
		x = vx + vw - 10 - w;
	} else if ( x < 2 ) {
		x = 2;
	}
	if ( y + h >= vy + vh - 10 ) {
		y = my - h - 10;
	}
	this.$root.style.top = y + 'px';
	this.$root.style.left = x + 'px';
}
ToolTip.setContent = function (content) {
	var ctt = null;
	if ( content ) {
		if ( Object.isElement(content) ) {
			ctt = content;
		} else {
			ctt = _ct(content);
		}
		this.$root.update().appendChild(ctt);
	} else {
		this.$root.update();
		this.hide();
	}
}
ToolTip.update = function (content, event) {
	this.show();
	this.setContent(content);
	this.positionate(event);
}



/**
 * To handle all editors of a fragment edition
 */
var TrFragmentEditor = Class.create({
	initialize: function (panel) {
		this.edSource = new TrEditor(this, EDITOR_SOURCE);
		this.edSource.setReadOnly();
		this.edGeneral = new TrEditor(this, EDITOR_GENERAL);
		this.editors = new Array();
		this.$root = _ce('div', {className: 'tr-list tr-frag-editor'},
			[this.edSource.getElement(), this.edGeneral.getElement()]);
		this.panel = panel;
		this.lastRefresh = null;
		this.refreshing = false;
		this.loadedFor = null;
		this.timestamp = null;
		this.ev_timer();
	},
	
	
	getRemainingLockTime: function () {
		var d = (new Date()).getTime();
		if ( !this.timestamp ) return -1;
		d = TrTool.getLockDuration() - Math.round((d - this.timestamp)/1000);
		if ( d < 0 ) return 0;
		return d;
	},
	
	
	load: function (pageId, md5) {
		var me = this, lang = TrTool.getLang(), key = lang  + '-' + pageId  + '-' + md5;
		if ( this.refreshing || !lang || !pageId || !md5 ) {
			return false;
		}
		if ( this.loadedFor == key && this.lastRefresh && (((new Date()).getTime() - this.lastRefresh.getTime())  / 1000) <= TrTool.getLockDuration() ) {
			return true;
		}
		this.refreshing = true;
		tr_ajax_request(AR_LOAD_FRAG_INFOS_AND_LOCK, {page_id: pageId, md5: md5}, {
			after: function(response) {
				if ( response && !response.error ) {
					me.loadedFor = key;
					me.lastRefresh = new Date();
				}
				me.refreshing = false;
				me.startTimer();
			},
			// startup func
			before: function () {
				me.reset();
			}
		}, 'Loading fragment\'s details');
		return true;
	},
	
	
	startTimer: function () {
		this.timestamp = (new Date()).getTime();
	},
	
	
	getElement: function () {
		return this.$root;
	},


	disable: function (yes) {
		var y = (yes == undefined ? true : !!yes);
		this.edGeneral.disable(y);
		this.editors.each(function (ed) {
			ed.disable(y);
		});
	},
	
	
	reset: function () {
		var ed;
		this.timestamp = null;
		this.edSource.reset();
		this.edGeneral.reset();
		this.edGeneral.disable();
		while ( this.editors.length > 0 ) {
			ed = this.editors.pop(0);
			this.$root.removeChild(ed.getElement());
			ed = null;
		}
		this.loadedFor = null;
	},
	
	
	revert: function () {
		this.afterDataUpdate();
	},
	
	
	changed: function () {
		var changed = false;
		if ( this.edGeneral.changed() ) {
			return true;
		}
		this.editors.each(function (editor) {
			if ( editor.changed() ) {
				changed = true;
				throw $break;
			}
		});
		return changed;
	},
	
	
	saveAll: function () {
		this.edGeneral.save();
		this.editors.each(function (editor) {
			editor.save(this);
		}, this);
	},
	
	
	getMd5Hash: function () {
		return this.edSource.getMd5Hash();
	},
	
	
	getLang: function () {
		return this.edGeneral.getLang();
	},
	
	
	getSourceText: function () {
		return this.edSource.getOriginal();
	},
	
	
	getGeneralText: function (original) {
		return this.edGeneral['get' + (original ? 'Original' : 'Current')]();
	},
	
	
	isEditable: function () {
		return (!this.isLocked() && this.edSource.getData(KEY_LOCKED_BY, 0) == TrTool.getUserId());
	},
	
	
	isLocked: function () {
		return !!this.edSource.getData(KEY_LOCKED, true);
	},
	
	
	isInEdition: function () {
		var l = this.edSource.getData(KEY_LOCKED_BY, 0);
		return !!(l && l != 0 && l != TrTool.getUserId());
	},
	
	
	updateData: function (data) {
		var locId, lang = data.get(KEY_LANG_ABBRIV_SHORT), md5 = data.get(KEY_MD5_HASH);
		var ed = this.getEditorByLocationId(data.get(KEY_LOCATION_ID, -1), lang), t = '';
		
		if ( !lang || (this.getLang() && lang != TrTool.getOrigLang() && lang != this.getLang()) || (this.getMd5Hash() && md5 != this.getMd5Hash()) ) {
			if ( TrTool.isDev() ) {
				t = LF + data.inspect();
			}
			alert('Data synchronisation error, please reload the page.' + t);
			return false;
		}
		
		if ( !ed ) {
			ed = new TrEditor(this, EDITOR_DISTINCT);
			this.editors.push(ed);
			this.$root.appendChild(ed.getElement());
		}

		ed.updateData(data);
		return this.afterDataUpdate(ed);
	},
	
	
	getEditorByLocationId: function (locId, lang) {
		var lid, ed;
		if ( lang == TrTool.getOrigLang() || locId == undefined || locId === null || locId === false || locId == -1 ) {
			return this.edSource;
		}
		lid = parseInt(locId);
		if ( lid == 0 ) {
			return this.edGeneral;
		}
		ed = null;
		this.editors.each(function (editor) {
			if ( editor.getLocationId() == lid ) {
				ed = editor;
				throw $break;
			}
		}, this);
		return ed;
	},
	
	
	updateFragmentData: function () {
		var f = TrTool.pFrags.getFragmentByMd5(this.getMd5Hash(), true), nbuc = 0, nbt = 0;
		var d = new Hash();
		
		if ( !f ) {
			return false;
		}
		this.edSource.data.each(function (pair) {
			d.set(pair.key, pair.value);
		});
		// remove language info
		d.unset(KEY_LANG_ABBRIV_SHORT);
		if ( this.edGeneral.getTranslatorId() > 0 ) {
			nbt++;
			(this.edGeneral.getConfirmerId() == 0) && nbuc++;
		}
		d.set(KEY_GENERAL_T, nbt > 0 ? true : false);
		this.editors.each(function (editor) {
			if ( editor.getTranslatorId() > 0 ) {
				nbt++;
				if ( editor.getConfirmerId() == 0 ) nbuc++;
			}
		}, this);
		d.set(KEY_NB_TRANSLATED, nbt);
		d.set(KEY_NB_UNCONFIRMED, nbuc);
		f.updateData(d);
		return true;
	},
	
	
	afterDataUpdate: function (editor) {
		var ok = true, f;
		if ( !this.edSource.afterDataUpdate() ) return false;
		if ( !this.edGeneral.afterDataUpdate() ) return false;
		this.editors.each(function (editor) {
			if ( !editor.afterDataUpdate() ) {
				ok = false;
				throw $break;
			}
		});
		// TODO: show global infos for all frags
		this.updateFragmentData();
		return ok;
	},
	
	
	// events
	ev_timer: function () {
		var d = this.getRemainingLockTime();
		var t = '', s, m, cl = false;
		this.timer = null;
		if ( d == -1 ) {
			t = '';
			
		} else if ( d == 0 ) {
			this.disable();
			this.panel.eTitle.setCell(0, _ce('span', {style: {fontSize: '0.8em'}, T: 'Your lock has expired.'}));
			return;
			
		} else {
			m = Math.floor(d / 60);
			if ( m > 0 ) {
				s = d - (60 * m);
				if ( s > 0 ) {
					t = m + ' min. & ' + s + ' sec.';
				} else {
					t = m + ' min.';
				}
			} else {
				t = d + ' sec.';
			}
			// colors
			if ( d < 60 ) {
				cl = 'rgb(199, 47, 44)';
			} else if ( d < (TrTool.getLockDuration() / 3) ) {
				cl = 'rgb(247, 132, 0)';
			}
			t += ' until lock lease';
		}
		t = _ce('span', {style: {fontSize: '0.8em'}, T: t});
		if ( cl ) t.style.color = cl;
		this.panel.eTitle.setCell(0, t);
		this.timer = window.setTimeout(this.ev_timer.bind(this), 1000);
	}
});


var TrButton = Class.create({
	initialize: function (img, caption, tooltip) {
		this.$img = _ce('img', {alt: caption, src: img});
		this.$caption = _ce('td', {className: 'tr-caption'});
		this.$root = _ce('table', {className: 'tr-button', cellSpacing: 0, cellPadding: 0, title: '#'}, [
			['tbody', {}, [
				['tr', {}, [
					['td', {className: 'tr-icon'}, [this.$img]], this.$caption
				]]
			]]
		]);
		this.setCaption(caption);
		this.setToolTip(tooltip);
		
		// events
		this.$root.onmouseover = this.ev_mouseover.bindAsEventListener(this);
		this.$root.onmouseout = this.ev_mouseout.bindAsEventListener(this);
		this.$root.onclick = this.ev_click.bindAsEventListener(this);
	},
	
	
	getElement: function () {
		return this.$root;
	},
	
	
	setToolTip: function (text) {
		this.$root.title = text ?  text : '';
	},
	
	
	setCaption: function (text, tooltip) {
		this.$caption.update().appendChild(_ct(text ? text : ''));
		if ( tooltip != undefined ) {
			this.setToolTip(tooltip);
		}
	},
	
	
	setIcon: function (icon) {
		this.$img.src = icon;
	},
	
	
	disable: function (yes) {
		var d = !!(yes == undefined ? true : yes);
		this.$root[(d ?  'add' : 'remove') + 'ClassName']('tr-disabled');
	},
	
	
	isDisabled: function () {
		return (this.toolBar.isDisabled() || this.$root.hasClassName('tr-disabled'));
	},
	
	
	setVisible: function (yes) {
		this.$root[(yes == undefined ? 'show': (yes ? 'show' : 'hide'))]();
	},
	
	
	isVisible: function () {
		this.$root.visible();
	},
	
	
	
	// events ==================================================================
	ev_mouseover: function (e) {
		this.$root[(this.isDisabled() ?  'remove' : 'add') + 'ClassName']('tr-pointer');
		this.$root[(this.isDisabled() ?  'add' : 'remove') + 'ClassName']('tr-forbidden-pointer');
		this.$root[(this.isDisabled() ?  'remove' : 'add') + 'ClassName']('tr-mouseover');
	},
	
	
	ev_mouseout: function (e) {
		this.$root.removeClassName('tr-pointer');
		this.$root.removeClassName('tr-forbidden-pointer');
		this.$root.removeClassName('tr-mouseover');
	},
	
	
	ev_click: function (e) {
		if ( this.isDisabled() ) {
			return;
		}
		this.onclick && this.onclick(this, e);
	}
});


var TrEditorToolBar = Class.create({
	initialize: function (editor) {
		this.editor = editor;
		this.$tr = _ce('tr');
		this.$root = _ce('table', {className: 'tr-toolbar'}, [['tbody', {}, [this.$tr]]]);
		this.buttons = new Array();
		this.disabled = false;
	},
	
	
	setVisible: function (yes) {
		this.$root[(yes == undefined ? true : !!yes) ? 'show' : 'hide']();
	},
	
	
	appendButton: function (button) {
		button.toolBar = this;
		this.buttons.push(button);
		this.$tr.appendChild(_ce('td', {}, [button.getElement()]));
		return (this.buttons.length - 1);
	},
	
	
	createButton: function (img, caption, tooltip) {
		var res =  new TrButton(img, caption, tooltip);
		this.appendButton(res);
		return res;
	},
	
	
	getButton: function (index) {
		return this.buttons[index];
	},
	
	
	getElement: function () {
		return this.$root;
	},
	
	
	getTextarea: function () {
		return this.editor.$text;
	},
	
	
	disable: function (yes) {
		this.disabled = !!(yes == undefined ? true : yes);
		this.$root[(this.disabled ? 'add' : 'remove') + 'ClassName']('tr-disabled');
	},
	
	
	isDisabled: function () {
		return !!this.disabled;
	}
});


var TrEditor = Class.create({
	initialize: function (fragEditor, type) {
		this.text = '';
		this.type = type;
		this.data = new Hash();
		this.fragEditor = fragEditor;
		this.$text = _ce('textarea', {value: this.text});
		this.$title = _ce('div', {className: 'tr-title'});
		this.autoTrim = true;
		this.toolBar = new TrEditorToolBar(this);
		
		// TODO: add common buttons here, accessible by their name, like cut, copy, paste, ...
		this.btnConfirm = this.toolBar.createButton(IMG_ICO_CONFIRM, 'Confirm', 'Confirm the translation');
		this.btnConfirm.onclick = this.ev_clickOnConfirm.bind(this);
		
		this.btnSave = this.toolBar.createButton(IMG_ICO_SAVE, 'Save', 'Save your translation');
		this.btnSave.onclick = this.ev_clickOnSave.bind(this);
		
		this.btnRevert = this.toolBar.createButton(IMG_ICO_REVERT, 'Undo', 'Undo all changes');
		this.btnRevert.onclick = this.ev_clickOnRevert.bind(this);
		
		this.toolBar.setVisible(!(type == EDITOR_SOURCE));
		this.$root = _ce('div', {className: 'tr-editor'}, [this.$title, this.toolBar.getElement(), this.$text]);
		
		// events
		this.$text.observe('focus', this.ev_focus.bindAsEventListener(this));
		this.$text.observe('blur', this.ev_blur.bindAsEventListener(this));
		this.$text.observe('keyup', this.ev_change.bindAsEventListener(this));
		this.$text.observe('mouseup', this.ev_change.bindAsEventListener(this));
		this.afterDataUpdate();
	},
	
	
	getType: function () {
		return this.type;
	},
	
	
	getMd5Hash: function () {
		return this.getData(KEY_MD5_HASH);
	},
	
	
	getLang: function () {
		return this.getData(KEY_LANG_ABBRIV_SHORT);
	},
	
	
	getElement: function () {
		return this.$root;
	},
	
	
	getTextarea: function () {
		return this.$text;
	},
	
	
	getOriginal: function () {
		var r = this.getData(KEY_TEXT);
		if ( !r ) {
			return '';
		}
		return r;
	},
	
	
	getCurrent: function () {
		var t = new String(this.$text.value);
		t = this.autoTrim ? t.strip() : t;
		if ( this.getType() == EDITOR_DISTINCT && t == this.fragEditor.getGeneralText(true) ) {
			t = '';
		}
		return t;
	},
	
	
	setCurrent: function (text) {
		var t = new String(text ? text : '');
		if ( this.getType() == EDITOR_DISTINCT && t == '' ) {
			t = this.fragEditor.getGeneralText(true);
		}
		this.$text.value = this.autoTrim ? t.strip() : t;
	},
	
	
	setOriginal: function (text) {
		this.setData(KEY_TEXT, text);
	},
	
	
	setBoth: function (text) {
		this.setOriginal(text);
		this.$text.value = this.getOriginal();
	},
	
	
	getSelectedText: function() {
		var userSelection;
		if ( !this.$text.focus ) {
			return false;
		}
		this.$text.focus();
		if ( window.getSelection ) {
			userSelection = window.getSelection();
		} else if ( document.selection ) {
			userSelection = document.selection.createRange();
		}
		if ( userSelection.text ) {
			return userSelection.text;
		}
		return userSelection;
	},
	
	
	changed: function () {
		return (this.getOriginal() != this.getCurrent());
	},
	
	
	isEditable: function () {
		if ( !this.fragEditor.isEditable() ) {
			return false;
		}
		if ( !(TrTool.isAdmin() || TrTool.isModerator(TrTool.getLang()) || TrTool.isNinja()) && this.fragEditor.edGeneral.getData(KEY_CONFIRMED_BY, 0) != 0 ) {
			return false;
		}
		return !this.getData(KEY_LOCKED, true);
	},
	
	
	save: function (confirm) {
		var me = this, lang = this.getLang(), locId = this.getLocationId(), md5 = this.getMd5Hash();
		var isConfirm = (confirm == undefined ? false : !!confirm), translation = this.getCurrent();
		this.$text.value = this.getCurrent();
		if ( this.getOriginal() == this.$text.value && !isConfirm ) {
			return true;
		}
		if ( this.onsave && !this.onsave(this) ) {
			return false;
		}
		if ( this.fragEditor.refreshing || !lang || !md5 || lang != TrTool.getLang() ) {
			return false;
		}
		this.fragEditor.refreshing = true;
		tr_ajax_request(AR_SAVE_FRAG_INFOS, {language_abbriv: lang, lid: locId, md5: md5, t: translation}, {
			after: function(response) {
				me.fragEditor.afterDataUpdate(me);
				if ( !response || response.error ) {
					me.$text.value = translation;
				}
				me.fragEditor.refreshing = false;
			},
			// startup func
			before: function () {
			}
		}, isConfirm ? 'Confirming translation' : 'Saving translation', true);
		return true;
	},
	
	
	setTitle: function (content) {
		var e;
		if ( Object.isElement(content) ) {
			e = content;
		} else {
			e = _ct(content);
		}
		this.$title.update().appendChild(e);
	},
	
	
	revert: function () {
		this.setCurrent(this.getOriginal());
	},
	
	
	reset: function () {
		this.data = new Hash();
		this.$text.value = '';
	},
	
	
	disable: function (yes) {
		var y = (yes == undefined ? true : !!yes);
		this.setReadOnly(y);
		this.$root[(y ? 'add' : 'remove') + 'ClassName']('tr-disabled');
		this.toolBar.disable(y);
	},
	
	
	setReadOnly: function (yes) {
		var y = (yes == undefined ? true : !!yes)
		this.$text.readOnly = y;
	},
	
	
	setData: function (key, value) {
		if ( key == KEY_TEXT && this.autoTrim ) {
			this.data.set(key, (new String(value)).strip());
		}
		this.data.set(key, value);
	},
	
	
	getData: function (key, ifNotFound) {
		var d;
		if ( this.data.keys().indexOf(key) >= 0 ) {
			d = this.data.get(key);
			return (INTEGER_KEYS.indexOf(key) >= 0) ? parseInt(d) : d;
		}
		return ifNotFound == undefined ? null : ifNotFound;
	},
	
	
	updateData: function (data) {
		var ns = (this.getType() != EDITOR_SOURCE), nu = false;
		data.each(function (pair) {
			if ( ns && (pair.key == KEY_LOCKER_PROFILE || pair.key == KEY_LOCKED_BY) ) {
				this.fragEditor.edSource.setData(pair.key, pair.value);
				nu = true;
			} else {
				this.setData(pair.key, pair.value);
			}
		}, this);
		if ( nu ) {
			this.fragEditor.edSource.afterDataUpdate();
		}
		this.afterDataUpdate();
	},
	
	
	getLocationId: function () {
		return this.getData(KEY_LOCATION_ID, 0);
	},
	
	
	getTranslatorId: function () {
		return this.getData(KEY_TRANSLATED_BY, 0);
	},
	
	
	getConfirmerId: function () {
		return this.getData(KEY_CONFIRMED_BY, 0);
	},
	
	
	afterDataUpdate: function (revert) {
		var m, isSrc = !!(this.getType() == EDITOR_SOURCE), t, tt, isGen = !!(this.getType() == EDITOR_GENERAL);
		if ( revert == undefined || revert ) this.revert();
		t = this.getCurrent();
		m = this.changed();
		this.$root[(!isSrc && !isGen && this.fragEditor.edGeneral.getOriginal() == '') ? 'hide' : 'show']();
		this.disable(isSrc || !this.isEditable());
		this.btnSave.disable(isSrc || !m);
		this.btnRevert.disable(isSrc || !m);
		this.btnConfirm.disable(isSrc || m || !t || this.getConfirmerId() || !(TrTool.isAdmin() || TrTool.isModerator() || TrTool.isNinja()));
		
		tt = _ce('span');
		if ( isSrc ) {
			tt.appendChild(_ct('Original text of the fragment'));
		} else {
			if ( isGen ) {
				tt.appendChild(_ct('General translation'));
			} else {
				tt.appendChild(_ct('Local translation #' + this.getLocationId()));
				if ( t == '' ) {
					tt.appendChild(_ce('span', {style: {color: 'green'}, T: (m ? ' (will use general one)' : ' (using general one)')}));
				}
			}
			if ( m ) {
				tt.appendChild(_ce('span', {}, [
					['span', {style: {color: 'red'}, T: ' *'}]
				]));
			}
		}
		this.setTitle(tt);
		
		TrTool.updateTitle();
		return true;
	},
	
		
	showHelp: function (e) {
		var p, html, d, h, noConf = false;
		
		TrTool.clearHelp();
		
		if ( !(TrTool.isAdmin() || TrTool.isModerator() || TrTool.isNinja()) && this.fragEditor.edGeneral.getData(KEY_CONFIRMED_BY, 0) != 0 ) {
			noConf = true;
			h = TrTool.createHelp('Confirmed');
			h.setHtmlContent('This fragment has been collectively confirmed. It\'s locked. If you need to edit it, please contact your team leader.');
		}

		p = this.fragEditor.edSource.getData(KEY_LOCKER_PROFILE);
		if ( p ) {
			h = TrTool.createHelp('Locked by');
			h.setHtmlContent(p);
			h.setRightAlign();
			if ( this.fragEditor.edSource.getData(KEY_LOCKED_BY) != TrTool.getUserId() ) {
				h.getContent().addClassName('tr-locked');
			}
		}
		
		if ( this.getType() != EDITOR_SOURCE ) {
			p = this.getData(KEY_TRANSLATOR_PROFILE);
			if ( p ) {
				d = this.getData(KEY_TRANSLATED_DATE);
				html = p + '<div>When&nbsp;: ' + (d ? d : '?') + '</div>';
				h = TrTool.createHelp('Translated by');
				h.setHtmlContent(html);
				h.setRightAlign();
			}

			p = this.getData(KEY_CONFIRMER_PROFILE);
			if ( !noConf && p ) {
				d = this.getData(KEY_CONFIRMED_DATE);
				html = p + '<div>When&nbsp;: ' + (d ? d : '?') + '</div>';
				h = TrTool.createHelp('Confirmed by');
				h.setHtmlContent(html);
				h.setRightAlign();
			}
		}
	},
	
	
	// events ==================================================================
	ev_focus: function (e) {
		this.onfocus && this.onfocus(this, e);
		this.showHelp();
	},
	
	
	ev_blur: function (e) {
		this.afterDataUpdate(false);
		this.onblur && this.onblur(this, e);
	},
	
	
	ev_change: function (e) {
		this.afterDataUpdate(false);
		this.onchange && this.onchange(this, e);
	},
	
	
	ev_clickOnRevert: function (button, e) {
		this.revert();
		this.afterDataUpdate(false);
	},
	
	
	ev_clickOnConfirm: function (button, e) {
		this.save(true);
	},
	
	
	ev_clickOnSave: function (button, e) {
		this.save();
	}
});



var TrListItem = Class.create({
	initialize: function (data, text) {
		this.$root = _ce('div', {className: 'tr-item', title: '#'});
		if ( text != undefined ) {
			this.$root.appendChild(_ce('span', {T: text}));
		}
		this.$root.onmouseover = this.ev_mouseover.bindAsEventListener(this);
		this.$root.onmouseout = this.ev_mouseout.bindAsEventListener(this);
		this.$root.onclick = this.ev_click.bindAsEventListener(this);
		this.$root.ondblclick = this.ev_doubleClick.bindAsEventListener(this);
		this.data = $H(data);
		this.list = null;
	},
	
	
	isVisible: function () {
		return this.$root.visible();
	},
	
	
	isDisabled: function () {
		return this.$root.hasClassName('tr-disabled');
	},
	
	
	setAllData: function (data) {
		this.data = $H(data);
	},
	
	
	setData: function (key, value) {
		this.data.set(key, value);
	},
	
	
	getData: function (key, ifNotFound) {
		var d;
		if ( !this.data ) {
			return ifNotFound == undefined ? null : ifNotFound;;
		}
		if ( this.data.keys().indexOf(key) >= 0 ) {
			d = this.data.get(key);
			return (INTEGER_KEYS.indexOf(key) >= 0) ? parseInt(d) : d;
		}
		return ifNotFound == undefined ? null : ifNotFound;
	},
	
	
	resetData: function () {
		var me = this;
		if ( !this.data ) {
			return;
		}
		$A(arguments).each(function (key) {
			me.data.unset(key);
		});
	},
	
	
	disable: function(disabled) {
		this.$root[((disabled == undefined || disabled) ? 'add' : 'remove') + 'ClassName']('tr-disabled');
	},
	
	
	getSortValueForKey: function (key) {
		return this.getData(key);
	},
	
	
	select: function () {
		if ( this.list && this.list.selected && this.list.selected == this ) {
			return true;
		}
		if ( this.onselect && !this.onselect((this.list && this.list.selected) ? this.list.selected : null) ) {
			return false;
		}
		if ( this.list && this.list.selected && !this.list.selected.unselect(this) ) {
			return false;
		}
		this.$root.addClassName('tr-selected');
		this.list.selected = this;
		TrTool.updateTitle();
		this.ev_mouseover();
		this.ev_mouseout();
	},
	
	
	isSelected: function () {
		return this.$root.hasClassName('tr-selected');
	},
	
	
	unselect: function (nextSelected) {
		if ( this.list && !this.list.selected )  {
			this.$root.removeClassName('tr-selected');
			return true;
		}
		if ( this.onunselect && !this.onunselect(nextSelected) ) {
			return false;
		}
		this.$root.removeClassName('tr-selected');
		if ( this.list ) this.list.selected = null;
		return true;
	},
	
	
	isSelectable: function () {
		return (this.isVisible() && !this.isDisabled());
	},
	
	
	// events ============================================================
	ev_click: function (e) {
		if ( !this.isSelectable() ) {
			return;
		}
		this.select();
	},
	
	
	ev_mouseover: function (e) {
		if ( this.isSelectable() ) {
			if ( !this.isSelected() ) {
				this.$root.addClassName('tr-mouseover');
			}
			this.$root.removeClassName('tr-forbidden-pointer');
		} else {
			this.$root.addClassName('tr-forbidden-pointer');
		}
	},
	
	
	ev_mouseout: function (e) {
		this.$root.removeClassName('tr-mouseover');
	},
	
	
	ev_doubleClick: function (e) {
		this.list && this.list.panel && this.list.panel.nextPanel && this.list.panel.nextPanel.expand();
	}
});


var TrList = Class.create({
	initialize: function (panel) {
		this.$root = _ce('div', {className: 'tr-list'});
		this.items = new Array();
		this.selected = null;
		this.panel = panel ? panel : null;
	},
	
	
	makeParFx: function () {
		var par = false;
		var elem = this.$root.firstChild;
		while ( elem ) {
			if ( Object.isElement(elem) && elem.nodeName.toLowerCase() == 'div' && elem.visible() ) {
				elem[(par ? 'remove' : 'add') + 'ClassName']('tr-odd');
				elem[(par ? 'add' : 'remove') + 'ClassName']('tr-par');
				par = !par;
			}
			elem = elem.nextSibling;
		}
	},
	
	
	remove: function (item) {
		if ( item.list != this ) {
			return item;
		}
		this.items.remove(this.items.indexOf(item));
		this.$root.removeChild(item.$root);
		item.list = null;
		if ( item.isVisibled() ) {
			this.makeParFx();
		}
		return item;
	},
	
	
	removeAll: function () {
		var item, pfx = false;
		while ( item = this.items.pop() ) {
			this.$root.removeChild(item.$root);
			item.list = null;
			pfx = pfx || item.isVisible();
		}
		if ( pfx ) {
			this.makeParFx();
		}
	},
	
	
	append: function (anItem) {
		var l;
		if ( anItem.list ) {
			if ( anItem.list == this ) {
				return anItem;
			}
			l = anItem.list;
			l.remove(anItem);
		}
		anItem.list = this;
		this.items.push(anItem);
		this.$root.appendChild(anItem.$root);
		return anItem;
	},
	
	
	appendItem: function(data, text) {
		var anItem = new TrListItem(data, text);
		this.append(anItem);
		return anItem;
	},
	
	
	count: function () {
		return this.items.length;
	},
	
	
	getItemByKeyValue: function (key, value) {
		var res = null;
		this.items.each(function (item) {
			if ( !res && item.getData(key) == value ) {
				res = item;
				throw $break;
			}
		});
		return res;
	},
	
	
	countVisible: function () {
		var nb = 0;
		this.items.each(function (item) {
			if ( item.isVisible() ) {
				nb++;
			}
		});
		return nb;
	},
	
	
	getSortedBy: function (key) {
 		return this.items.sortBy(function (item) {
			return item.getSortValueForKey(key);
		});
	},
	
	
	filter: function (filter) {
		this.items.each(function (item) {
			item.$root.hide();
		});
		this.items.findAll(filter).each(function (item) {
			item.$root.show();
		});
		this.makeParFx();
	},
	
	
	sortBy: function (key) {
		var me = this;
		this.getSortedBy(key).each(function (item) {
			me.$root.appendChild(item.$root);
		});
		this.makeParFx();
	}
});


/**
 * Language list
 */
var TrLanguageList = Class.create(TrList, {
	initialize: function ($super, panel) {
		$super(panel);
		this.$root.addClassName('tr-language');
		this.loaded = false;
	},
	
	
	loadAll: function () {
		var me = this;
		if ( this.loaded ) return true;
		tr_ajax_request(AR_LOAD_LANGUAGES, {}, {
			after: function(response){
				if ( response && !response.error ) {
					me.loaded = true;
					me.panel.applySearch();
				}
			}}, 'Loading languages');
		return true;
	}
});
var TrLanguageListItem = Class.create(TrListItem, {
	initialize: function ($super, data) {
		$super(data, data.get(KEY_LANGUAGE_NAME));
	},
	
	
	onselect: function (prevItem) {
		var h;
		TrTool.clearHelp();
		h = TrTool.createHelp('Language details');
		h.setContent(_ce('ul', {}, [
			['li', {T: 'Name: ' + this.getData(KEY_LANGUAGE_NAME)}],
			['li', {T: 'Local name: ' + this.getData(KEY_LANGUAGE_ORIG_NAME)}],
			['li', {T: 'Abbrv: ' + this.getData(KEY_LANGUAGE_ABBRIV)}],
			['li', {T: 'Identifier: ' + this.getData(KEY_LANGUAGE_ID)}],
			['li', {T: 'Team member(s): ' + this.getData(KEY_NB_TRANSLATORS)}]
		]));
		return true;
	}
});



/**
 * Pages list
 */
var TrPageList = Class.create(TrList, {
	initialize: function ($super, panel) {
		$super(panel);
		this.$root.addClassName('tr-page');
		this.lastRefresh = null;
		this.refreshing = false;
		this.loadedFor = null;
	},
	
	
	loadInfos: function (force) {
		var me = this, oldId = this.selected, key = TrTool.getLang();
		if ( this.refreshing ) {
			return false;
		}
		if ( !force && this.loadedFor == key && this.lastRefresh && (((new Date()).getTime() - this.lastRefresh.getTime()) / 1000) <= REFRESH_PAGES_DELAY_SECS ) {
			return true;
		}
		
		this.refreshing = true;
		oldId = oldId ? oldId.getData(KEY_ID) : null;
		tr_ajax_request(AR_LOAD_PAGES_INFOS, {}, {
			after: function(response) {
				if ( response && !response.error ) {
					me.loadedFor = key;
					me.panel.applySearch();
					me.lastRefresh = new Date();
				}
				oldId && me.panel.selectPage(oldId);
				me.refreshing = false;
			},
			// startup func
			before: function () {
				me.panel.resetPageDatas();
			}
		}, 'Loading pages\' infos');
		return true;
	}
});
var TrPageListItem = Class.create(TrListItem, {
	initialize: function ($super, data) {
		var el;
		if ( data.keys().indexOf(KEY_LOCKED) < 0 ) {
			data.set(KEY_LOCKED, true);
		}
		$super(data);
		// making the display
		this.$imgStatus = _ce('img', {src: IMG_ICO_PAGE_NOT_FT, title: '#'});
		this.$status = _ce('span');
		el = _ce('div', {className: 'tr-status'}, [this.$imgStatus, ['br'], this.$status]);
		this.$root.appendChild(el);

		this.$label = _ce('span', {T: this.getData(KEY_LABEL, ''), title: '#'});
		this.$lnkNavigate = _ce('a', {href: '#', target: '_blank'}, [['img', {src: IMG_ICO_NAVIGATE, alt: 'Click here to open the real page in a new window, in the language you\'re actually translating to.'}]]);
		this.$btnClean = _ce('img', {className: 'tr-pointer', src: IMG_ICO_CLEAN, title: 'Click here to detach all fragments from the page'});
		this.$btnClean.onclick = this.ev_clickOnClean.bindAsEventListener(this);
		this.$btnClearCache = _ce('img', {className: 'tr-pointer', src: IMG_ICO_CLEAR_CACHE, title: 'Click here to clear all the translation from the cache for this page (use this if you want to see the translations on a page you just cleaned, after some fragments got registered already).'});
		this.$btnClearCache.onclick = this.ev_clickOnClearCache.bindAsEventListener(this);
		this.$imgLock = _ce('img', {src: IMG_ICO_LOCKED, title: '#'});
		el = _ce('div', {className: 'tr-label'}, [this.$label, this.$lnkNavigate, this.$btnClean, this.$btnClearCache, this.$imgLock]);
		this.$root.appendChild(el);

		this.$infos = _ce('div', {className: 'tr-infos', T: '-'});
		this.$root.appendChild(this.$infos);
		// toolbar inserted in the help when a page is selected
		this.$btnLockForModerator = _ce('li', {T: 'Moderators', className: 'tr-pointer tr-locked', title: '#'});
		this.$btnLockForModerator.onclick = this.ev_clickOnLockFor.bindAsEventListener(this, PERM_CODE_MODERATOR);
		this.$btnLockForTranslator = _ce('li', {T: 'Translators', className: 'tr-pointer tr-locked', title: '#'});
		this.$btnLockForTranslator.onclick = this.ev_clickOnLockFor.bindAsEventListener(this, PERM_CODE_TRANSLATOR);
		this.$btnUnlock = _ce('li', {T: 'Not locked', className: 'tr-pointer tr-locked', title: '#'});
		this.$btnUnlock.onclick = this.ev_clickOnLockFor.bindAsEventListener(this, PERM_CODE_NOBODY);
		this.lockButtons = new Array();
		this.lockButtons.push(this.$btnLockForModerator);
		this.lockButtons.push(this.$btnLockForTranslator);
		this.lockButtons.push(this.$btnUnlock);
		this.$lockToolBar = _ce('ul', {className: 'tr-lock-tb'}, this.lockButtons);
		
		// update the GUI
		this.afterDataUpdate();
	},
	
	
	hasPath: function () {
		return (this.getData(KEY_FILE_PATH, '') != '');
	},
	
	
	needTranslation: function () {
		return ((this.getData(KEY_NB_GEN, 0) - this.getData(KEY_NB_GEN_TRANSLATED, 0) > 0) || (this.getData(KEY_NB_GEN, 0) == 0));
	},
	
	
	needConfirmation: function () {
		return (this.getData(KEY_NB_UNCONFIRMED, 0) > 0);
	},
	
	
	isFullyTranslated: function () {
		return !(this.needConfirmation() || this.needTranslation());
	},
	
	
	afterDataUpdate: function () {
		var img, text, nbgr, cn, tt, lkf, lfm, lft, lf_, lcn;
		// label
		this.$label.update().appendChild(_ct(this.getData(KEY_LABEL, '')));
		// stats
		text = new Array();
		text.push(this.getData(KEY_NB_GEN, 0) + ' frags.');
		text.push((this.getData(KEY_NB_TRANSLATED, 0) + this.getData(KEY_NB_GEN_TRANSLATED, 0)) + ' trans.');
		nbgr = this.getData(KEY_NB_GEN, 0) - this.getData(KEY_NB_GEN_TRANSLATED, 0);
		text.push(nbgr + ' gen. to trans.');
		text.push(this.getData(KEY_NB_UNCONFIRMED, 0) + ' to confirm');
		this.$infos.update().appendChild(_ct(text.join(', ')));
		// icon
		this.$label.title = this.getData(KEY_LABEL, '');
		if ( nbgr > 0 || this.getData(KEY_NB_GEN, 0) == 0 ) {
			img = IMG_ICO_PAGE_NOT_FT;
			cn = 'tr-not-translated';
			if ( this.getData(KEY_NB_GEN, 0) == 0 ) { 
				tt = 'Not built';
				this.$label.title = 'Go to the page to create the fragments';
			} else {
				tt = 'Not fully translated'
			}
			
		} else if ( this.getData(KEY_NB_UNCONFIRMED, 0) > 0 ) {
			img = IMG_ICO_PAGE_NOT_FC;
			tt = 'One or more confirmation needed';
			cn = 'tr-translated';
			
		} else {
			img = IMG_ICO_PAGE_FT;
			tt = 'Fully translated';
			cn = 'tr-confirmed';
		}
		this.$imgStatus.src = img;
		this.$imgStatus.alt = tt;
		this.$imgStatus.title = tt;
		this.$status.update().appendChild(_ct(this.getData(KEY_NB_GEN_TRANSLATED, 0) + ' / ' + this.getData(KEY_NB_GEN, 0)));
		this.$status.className = cn;
		
		this.disable(this.isLocked());
		this.$imgLock[this.isLocked() ? 'show' : 'hide']();
		if ( this.getData(KEY_NB, 0) == 0 ) {
			this.$imgLock.hide();
			this.$root.hide();

		} else if ( this.isLocked() ) {
			this.$imgLock.toolTip.setContent('Locked by an admin, moderator or language ninja.');
			if ( !(TrTool.isAdmin() || TrTool.isNinja() || TrTool.isModerator()) ) {
				this.$root.hide();
			}
		}
		this.$lnkNavigate[this.hasPath() ? 'show' : 'hide']();
		this.$lnkNavigate.href = this.getData(KEY_FILE_PATH, '#') + '?user_language=' + TrTool.getLang();
		
		// locked for
		this.$btnLockForTranslator.removeClassName('tr-locked');
		this.$btnLockForTranslator.title = 'Not locked';
		this.$btnLockForModerator.removeClassName('tr-locked');
		this.$btnLockForModerator.title = 'Not locked';
		this.$btnUnlock.removeClassName('tr-locked');
		this.$btnUnlock.title = '';
		lkf = this.getData(KEY_LOCKED_FOR);
		this.$btnLockForModerator.addClassName('tr-disabled');
		this.$btnLockForTranslator.addClassName('tr-disabled');
		this.$btnUnlock.addClassName('tr-disabled');
		lfm = (lkf == PERM_CODE_EVERYBODY || lkf == PERM_CODE_MODERATOR);
		lft = (lkf == PERM_CODE_EVERYBODY || lkf == PERM_CODE_MODERATOR || lkf == PERM_CODE_TRANSLATOR);
		lf_ = (lkf == PERM_CODE_NOBODY);
		lcn = '';
		if ( lft ) {
			this.$btnLockForTranslator.addClassName('tr-locked');
			this.$btnLockForTranslator.title = 'Locked';
			lcn = 'tr-orange';
		}
		if ( lfm ) {
			this.$btnLockForModerator.addClassName('tr-locked');
			this.$btnLockForModerator.title = 'Locked';
			lcn = 'tr-red';
		}
		this.$label.className = lcn;
		if ( lf_ ) {
			this.$btnUnlock.addClassName("tr-locked");
			this.$btnUnlock.title = 'Not locked';
		}
		if ( TrTool.isModerator() && !this.isLocked() && lkf != PERM_CODE_TRANSLATOR ) {
			this.$btnLockForTranslator.removeClassName('tr-disabled');
			this.$btnLockForTranslator.title += ' - ' + 'Click to lock for translators';
		}
		if ( TrTool.isNinja() && !this.isLocked() && lkf != PERM_CODE_MODERATOR ) {
			this.$btnLockForModerator.removeClassName('tr-disabled');
			this.$btnLockForModerator.title += ' - ' + 'Click to lock for moderators & translators';
		}
		if ( TrTool.isModerator() && !this.isLocked() && lkf != PERM_CODE_NOBODY ) {
			this.$btnUnlock.title = 'Click to unlock';
			this.$btnUnlock.removeClassName('tr-disabled');
		}
		this.$btnClean[TrTool.isNinja() || TrTool.isAdmin() ? 'show' : 'hide']();
		this.$btnClearCache[TrTool.isNinja() || TrTool.isAdmin() ? 'show' : 'hide']();
		
		if ( !this.isSelectable() && this.isSelected() ) {
			this.unselect();
		} else if ( this.isSelected() ) {
			this.onselect();
		}
		return true;
	},
	
	
	isLocked: function () {
		return !!this.getData(KEY_LOCKED, true);
	},
	
	
	updateData: function (data) {
		var me = this;
		data.each(function (pair) {
			if ( pair.key == KEY_ID ) {
				return;
			}
			me.setData(pair.key, pair.value);
		});
		return this.afterDataUpdate();
	},
	
	
	lockFor: function (permCode) {
		var me = this;
		tr_ajax_request(AR_LOCK_PAGE, {page_id: this.getData(KEY_ID), lock_for: permCode}, {
			after: function(response) {
				if ( response && !response.error ) {
					me.afterDataUpdate();
				}
			}
		}, 'Locking page');
	},


	clean: function () {
		var me = this;
		if ( !confirm('This will detach all fragments from the page "' + this.getData(KEY_LABEL) + '", are you sure?') ) return;
		tr_ajax_request(AR_CLEAN_PAGE, {page_id: this.getData(KEY_ID)}, {
			after: function(response) {
				if ( response && !response.error ) {
					me.afterDataUpdate();
				}
			}
		}, 'Cleaning page');
	},


	clearCache: function () {
		var me = this;
		tr_ajax_request(AR_CLEAR_PAGE_CACHE, {page_id: this.getData(KEY_ID)}, {}, 'Clearing page\'s cache');
	},


	isVisible: function () {
		if ( this.getData(KEY_LABEL, '') == '' ) {
			return false;
		}
		if ( (this.getData(KEY_NB, 0) == 0 || this.isLocked()) && !(TrTool.isAdmin() || TrTool.isNinja()) ) {
			return false;
		}
		return true;
	},

	
	// events ==================================================================
	onselect: function (prevItem) {
		var h;
		TrTool.clearHelp();
		h = TrTool.createHelp('Page details');
		h.setContent(_ce('ul', {}, [
			['li', {T: 'Last rebuild: ' + this.getData(KEY_LAST_REBUILD_AT, '?')}],
			['li', {T: this.getData(KEY_NB_GEN, 0) + ' fragments'}],
			['li', {T: this.getData(KEY_NB_GEN_TRANSLATED, 0) + ' with general translation'}],
			['li', {T: this.getData(KEY_NB_TRANSLATED, 0) + ' distinct translation(s)'}],
			['li', {T: this.getData(KEY_NB_UNCONFIRMED, 0) + ' unconfirmed translation(s)'}]
		]));
		
		h = TrTool.createHelp('Lock level');
		h.setContent(_ce('div', {}, [
			['#', 'Here is the user level for which the page has been locked:'],
			this.$lockToolBar
		]));
		return true;
	},


	ev_clickOnLockFor: function (e, permCode) {
		if ( !e.target.hasClassName('tr-disabled') ) {
			this.lockFor(permCode);
		}
	},


	ev_clickOnClean: function (e) {
		if ( TrTool.isNinja() || TrTool.isAdmin() ) this.clean();
	},


	ev_clickOnClearCache: function (e) {
		if ( TrTool.isNinja() || TrTool.isAdmin() ) this.clearCache();
	}
});



/**
 * Fragments list
 */
var TrFragmentList = Class.create(TrList, {
	initialize: function ($super, panel) {
		$super(panel);
		this.$root.addClassName('tr-fragment');
		this.refreshing = false;
		this.lastRefresh = null;
		this.loadedFor = null;
	},
	
	
	loadAll: function (pageId, loadTranslatedFrags) {
		var me = this, oldId = this.selected, key = TrTool.getLang() + '-' + pageId;
		if ( this.refreshing || !pageId ) {
			return false;
		}
		if ( this.loadedFor == key && (!loadTranslatedFrags || TrTool.pFrags.translatedFragsLoaded) && this.lastRefresh && (((new Date()).getTime() - this.lastRefresh.getTime()) / 1000) <= REFRESH_FRAGS_DELAY_SECS ) {
			return true;
		}
		TrTool.pFrags.translatedFragsLoaded = !!loadTranslatedFrags;
		
		this.refreshing = true;
		oldId = oldId ? oldId.getData(KEY_MD5_HASH) : null;
		tr_ajax_request(AR_LOAD_PAGE_FRAGMENTS, {page_id: pageId, translated: !!loadTranslatedFrags ? 'y' : 'n'}, {
			after: function(response) {
				if ( response && !response.error ) {
					me.panel.applySearch();
					me.loadedFor = key;
					me.lastRefresh = new Date();
				}
				oldId && me.panel.selectFragment(oldId);
				me.refreshing = false;
			},
			// startup func
			before: function () {
				me.removeAll();
			}
		}, 'Loading page\'s fragments');
		return true;
	}
});
var TrFragmentListItem = Class.create(TrListItem, {
	initialize: function ($super, data) {
		var el;
		if ( data.keys().indexOf(KEY_LOCKED) < 0 ) {
			data.set(KEY_LOCKED, true);
		}
		$super(data);
		// making the display
		this.$imgStatus = _ce('img', {src: IMG_ICO_FRAG_NOT_T, title: '#'});
		this.$imgLock = _ce('img', {src: IMG_ICO_LOCKED, title: '#'});
		el = _ce('div', {className: 'tr-status'}, [this.$imgLock, this.$imgStatus]);
		this.$root.appendChild(el);
		this.$text = _ce('div', {className: 'tr-label', title: '#'});
		this.$root.appendChild(this.$text);
		this.$infos = _ce('div', {className: 'tr-infos', T: '-'});
		this.$root.appendChild(this.$infos);
		
		// update the GUI
		this.afterDataUpdate();
	},
	
	
	needTranslation: function () {
		return !this.getData(KEY_GENERAL_T, false);
	},
	
	
	needConfirmation: function () {
		return (this.getData(KEY_NB_UNCONFIRMED, 0) > 0);
	},
	
	
	isTranslated: function () {
		return !(this.needConfirmation() || this.needTranslation());
	},
	
	
	afterDataUpdate: function () {
		var img, text, cn, tt, sl;
		// text
		this.$text.update().appendChild(_ct(this.getData(KEY_TEXT, '').replace(/\s+/, ' ')));
		this.$text.title = this.getData(KEY_TEXT, '');
		// stats
		text = new Array();
		text.push(this.getData(KEY_NB_TRANSLATED, 0) + ' translated');
		text.push(this.getData(KEY_NB_UNCONFIRMED, 0) + ' to confirm');
		this.$infos.update().appendChild(_ct(text.join(', ')));
		// icon
		if ( !this.getData(KEY_GENERAL_T, false) ) {
			img = IMG_ICO_FRAG_NOT_T;
			tt = 'Not translated';
			//cn = 'tr-not-translated';
			
		} else if ( this.getData(KEY_NB_UNCONFIRMED, 0) > 0 ) {
			img = IMG_ICO_FRAG_NOT_C;
			tt = 'Need one or more confirmation';
			//cn = 'tr-translated';
			
		} else {
			img = IMG_ICO_FRAG_OK;
			tt = 'General translation present and confirmed';
			//cn = 'tr-confirmed';
		}
		this.$imgStatus.src = img;
		this.$imgStatus.alt = tt;
		this.$imgStatus.title = tt;
		
		this.disable(!this.isEditable());
		sl = false;
		this.$imgLock.src = this.isLocked() ? IMG_ICO_LOCKED : IMG_ICO_WARNING;
		this.$imgLock.alt = this.isLocked() ? 'Locked' : 'In edition';
		if ( !this.isEditable() && this.getData(KEY_LOCKER_PROFILE) ) {
			this.$imgLock.toolTip.setHtmlContent('Currently edited by&nbsp;:<br/>' + this.getData(KEY_LOCKER_PROFILE));
			sl = true;
		} else if ( this.isCollectivelyLocked() ) {
			this.$imgLock.toolTip.setHtmlContent('Collectively locked');
			this.$imgLock.src = IMG_ICO_LOCKED;
			this.$imgLock.alt = 'Collectively locked';
			sl = true;
		}
		this.$imgLock[sl ? 'show' : 'hide']();
		
		if ( !this.isSelectable() && this.isSelected() ) {
			this.unselect();
		} else if ( this.isSelected() ) {
			this.onselect();
		}
		return true;
	},
	
	
	isLocked: function () {
		return !!this.getData(KEY_LOCKED, true);
	},


	isCollectivelyLocked: function () {
		return this.getData(KEY_GEN_CONFIRMED_BY, 0) != 0 && !(TrTool.isAdmin() || TrTool.isNinja() || TrTool.isModerator());
	},
	
	
	isInEdition: function () {
		var res = this.getData(KEY_LOCKED_BY, 0);
		return !(res == 0 || res == TrTool.getUserId())
	},
	
	
	isEditable: function () {
		/*if ( this.getData(KEY_GEN_CONFIRMED_BY, 0) != 0 && !(TrTool.isAdmin() || TrTool.isNinja() || TrTool.isModerator()) ) {
			return false;
		}*/
		return !this.isInEdition() && !this.isLocked();
	},
	
	
	updateData: function (data) {
		var me = this;
		data.each(function (pair) {
			if ( pair.key == KEY_ID ) {
				return;
			}
			me.setData(pair.key, pair.value);
		});
		return this.afterDataUpdate();
	},
	
	
	// events ==================================================================
	onselect: function (item) {
		var p, h;
		TrTool.clearHelp();
		// infos
		h = TrTool.createHelp('Frag. details');
		h.setContent(_ce('ul', {}, [
			['li', {T: 'General' + (this.getData(KEY_GENERAL_T, false) ? '' : ' not') + ' translated'}],
			['li', {T: (this.getData(KEY_NB_TRANSLATED , 0) - (this.getData(KEY_GENERAL_T, false) ? 1 : 0)) + ' distinct translation(s)'}],
			['li', {T: this.getData(KEY_NB_UNCONFIRMED , 0) + ' unconfirmed translation(s)'}]
		]));
		// collectively locked
		if ( this.getData(KEY_GEN_CONFIRMED_BY, 0) > 0 && !(TrTool.isAdmin() || TrTool.isModerator() || TrTool.isNinja()) ) {
			h = TrTool.createHelp('Confirmed');
			h.setHtmlContent('This fragment has been collectively confirmed. It\'s locked. If you need to edit it, please contact your team leader.');
		}
		// locker profile
		p = this.getData(KEY_LOCKER_PROFILE);
		if ( p ) {
			h = TrTool.createHelp('Locked by');
			h.setHtmlContent(p);
			h.setRightAlign();
			if ( this.getData(KEY_LOCKED_BY) != TrTool.getUserId() ) {
				h.getContent().addClassName('tr-locked');
			}
		}
		return true;
	}
});



/**
 * Main tool functions
 */
var TrTool = {
	initialized: false,
	$root: null,
	ready: false,
	data: null,
	helpBoxes: new Array(),
	
	initialize: function (data, abbriv, selectedPage) {
		this.data = $H(data);
		if ( this.initialized ) {
			return;
		}
		this._initialize(abbriv, selectedPage);
	},
	
	
	clearHelp: function () {
		this.helpBoxes.each(function (hb) {
			this.$helpContent.removeChild(hb.getElement());
		}, this);
		this.helpBoxes.clear();
	},
	
	
	createHelp: function (title, content) {
		var hb = new TrHelpBox(title, content);
		this.$helpContent.appendChild(hb.getElement());
		this.helpBoxes.push(hb);
		return hb;
	},
	
	
	checkVersion: function(version) {
		if ( !this.getVersion() || version != this.getVersion() ) {
			alert('A new version of the tool exists (version ' + version + ') but you have an old one in your cache (version ' + this.getVersion() + '). ' + LF + 'Refresh the page to get the new one plesae.');
			return false;
		}
		return true;
	},
	
	
	getVersion: function () {
		return this.data.get(KEY_VERSION);
	},
	
	
	getOrigLang: function () {
		return this.data.get(KEY_ORIG_LANGUAGE_ABBRIV);
	},
	
	
	isDev: function () {
		return this.data.get(KEY_IS_DEV);
	},
	
	
	isTranslator: function () {
		return this.data.get(KEY_IS_TRANSLATOR);
	},
	
	
	isNinja: function () {
		return this.data.get(KEY_IS_NINJA);
	},
	
	
	isAdmin: function () {
		return this.data.get(KEY_IS_ADMIN);
	},
	
	
	isModerator: function (lang) {
		var l = (lang == undefined) ? this.getLang() : lang;
		l = TrTool.pLangs.getLanguageByAbbriv(l);
		if ( l ) l = l.get(KEY_CAN_MODERATE_LANG);
		return !!(l && parseInt(l));
	},
	
	
	setReady: function (ready) {
		this.ready = !!(ready == undefined || ready);
	},
	
	
	isReady: function () {
		return this.ready;
	},
	
	
	getUserId: function() {
		return this.data.get(KEY_USER_ID);
	},
	
	
	getLockDuration: function () {
		return (this.data.get(KEY_LOCK_TIME, 0) * 60);
	},
	
	
	cbCopy: function (text) {
		if ( window.clipboardData && window.clipboardData.setData ) {
			window.clipboardData.setData('Text', text);
		} else {
			return false;
		} 
		return true;
	},
	
	
	getLang: function () {
		var res = TrTool.pLangs.getSelectedLanguage();
		return res ? res.get(KEY_LANGUAGE_ABBRIV) : null;
	},
	
	
	getPageId: function () {
		var res = this.pPages.getSelectedPage();
		return res ? res.get(KEY_ID) : null;
	},
	
	
	getFragmentMd5Hash: function () {
		var res = this.pFrags.getSelectedFragment();
		return res ? res.get(KEY_MD5_HASH) : null;
	},
	
	
	raw: function (data) {
		var sd;
		if ( data.type == 'lg' ) {
			this.pLangs.appendLanguage(data.data);
			
		} else if ( data.type == 'wp' ) {
			this.pPages.appendPage(data.data);
			
		} else if ( data.type == 'wpgi' ) {
			this.pPages.updatePageData(data.data);
			
		} else if ( data.type == 'fd' ) {
			if ( this.pFrags.updateFragmentData(data.data) ) {
				if ( this.pEdit.isExpanded() ) {
					this.pEdit.editor.updateData(data.data);
				}
				
			} else {
				this.pFrags.appendFragment(data.data);
			}
			
		} else if ( data.type == 'ft' ) {
			this.pEdit.editor.updateData(data.data);
			
		} else {
			alert('data.type "' + data.type + '": unkown type');
		}
	},
	
	updateTitle: function () {
		var t = '', d;
		if ( this.pLangs && this.pLangs.isExpanded() ) {
			d = this.pLangs.getSelectedLanguage();
			if ( d ) {
				t = d.get(KEY_LANGUAGE_NAME) + ' - Open "Pages" panel to choose a page';
			} else {
				t = 'Welcome to the translation tool - Select a language';
			}
			
		} else if ( this.pPages && this.pPages.isExpanded() ) {
			t = this.pLangs.getSelectedLanguage().get(KEY_LANGUAGE_NAME);
			d = this.pPages.getSelectedPage();
			if ( d ) {
				t = t + '/' + d.get(KEY_LABEL) + ' - Open "Frags" panel';
			} else {
				t = t + ' - Select a page';
			}
			
		} else if ( this.pFrags && this.pFrags.isExpanded() ) {
			t = this.pLangs.getSelectedLanguage().get(KEY_LANGUAGE_NAME) + '/' + this.pPages.getSelectedPage().get(KEY_LABEL);
			d = this.pFrags.getSelectedFragment();
			if ( d ) {
				t = t + '/' + d.get(KEY_MD5_HASH) + ' - Open "Edit" panel';
			} else {
				t = t + ' - Select a fragment';
			}

		} else if ( this.pEdit && this.pEdit.isExpanded() ) {
			t = this.pLangs.getSelectedLanguage().get(KEY_LANGUAGE_NAME) + '/' + this.pPages.getSelectedPage().get(KEY_LABEL);
			t += '/' + this.pEdit.editor.getMd5Hash() + ' - Edition';
			if ( this.pEdit.editor.changed() ) {
				t = _ce('span', {T: t}, [
					['span', {style: {color: 'red'}, T: ' *'}]
				]);
			}
		}
		
		this.header.setContent(t);
	},

	
	_initialize: function (abbriv, selectedPage) {
		var el;
		if ( this.$root ) {
			return;
		}
		this.$root = $('tr_tool');
		
		// header
		this.header = new TrHorizBar(HB_HEADER);
		// footer
		this.footer = new TrHorizBar(HB_FOOTER, 2);
		this.footer.setCell(1, 'version ' + this.getVersion());
		// developer js field and button
		if ( this.isDev() ) {
			this.$jsCode = _ce('input', {className: 'tr-code'});
			this.$jsBtnEval = _ce('input', {type: 'button', value: 'Eval'});
			this.$jsBtnEvalShow = _ce('input', {type: 'button', value: 'Eval & show'});
			this.$js = _ce('div', {className: 'tr-dev'}, [this.$jsCode, this.$jsBtnEval, this.$jsBtnEvalShow]);
			this.footer.getCell(0).appendChild(this.$js);
			this.footer.getCell(0).style.textAlign = 'left';
			this.$jsBtnEval.onclick = this.ev_clickOnEval.bindAsEventListener(this);
			this.$jsBtnEvalShow.onclick = this.ev_clickOnEvalShow.bindAsEventListener(this);
		}
		// panels space
		this.$panels = _ce('td');
		// help
		this.$helpContent = _ce('div', {className: 'tr-help'});
		this.$help = _ce('td', {className: 'tr-help'}, [_bi(), this.$helpContent]);
		
		// main element
		this.$main = _ce('table', {
			cellSpacing:	0,
			cellPadding:	0,
			className:		'tr-main'
		}, [
			['tbody', {}, [
				['tr', {}, [
					['td', {colSpan: 3}, [this.header.$root]]
				]],
				['tr', {}, [['td', {className: 'tr-tool-left'}, [_bi()]], this.$panels, this.$help]],
				['tr', {}, [
					['td', {colSpan: 3}, [this.footer.$root]]
				]]
			]]
		]);
		this.$root.appendChild(this.$main);
		
		// making panels
		TrPanel.setContainer(this.$panels);

		this.pLangs = new TrLanguagesPanel();
		this.pPages = new TrPagesPanel();
		this.pFrags = new TrFragmentsPanel();
		this.pEdit = new TrEditPanel();
		//this.pAdmin = new TrAdminPanel();
		
		
		// title
		this.updateTitle();

		// locading languages
		this.pLangs.expand();

		// setting height of the help content (-1 because of the 1px height of the blank image)
		this.$helpContent.style.height = (this.$help.getHeight() - 1) + 'px';

		// welcome
		this.createHelp(TR_HELP.WELCOME.TITLE, TR_HELP.WELCOME.BODY);
		
		
		// TODO: select lang and page if necessary
		
		this.$root.scrollTo();
		this.initialized = true;
		this.ready = true;
		return;
	},



	// events ==================================================================
	ev_clickOnEval: function (e) {
		var lang = this.pLangs.getSelectedLanguage(true);
		var page = this.pPages.getSelectedPage(true);
		var frag = this.pFrags.getSelectedFragment(true);
		var tool = TrTool;
		eval(this.$jsCode.value);
	},
	
	
	ev_clickOnEvalShow: function (e) {
		var lang = this.pLangs.getSelectedLanguage(true);
		var page = this.pPages.getSelectedPage(true);
		var frag = this.pFrags.getSelectedFragment(true);
		var tool = TrTool;
		alert(eval(this.$jsCode.value));
	}
	
};
