/*
from now on the conversion table will be set up as an array of arrays
[translit string, cyrillic letter, guess case (optional)]
if translit string is caseless, cyrillic letter is treated literally, otherwise it is toUpperCase()/toLowerCase().
if translit string is caseless and guess is present, figure out case based on context (for ' or `)

order tuples in order of translit preference
*/

// default latinica - russian-oriented customized tranlit readable both ways
var defaultLatinicaTable = [["shch", "\u0429"], ["yo", "\u0401"], ["y\\o", "\u044b\u043e"], ["y\\u", "\u044b\u0443"], ["y\\a", "\u044b\u0430"], ["zh", "\u0416"], ["ch", "\u0427"], ["sh", "\u0428"], ["yu", "\u042e"], ["ya", "\u042f"], ["e'", "\u042d"], ["eh", "\u042d"], ["sj", "\u0429"], ["a", "\u0410"], ["b", "\u0411"], ["v", "\u0412"], ["g", "\u0413"], ["d", "\u0414"], ["e", "\u0415"], ["z", "\u0417"], ["i", "\u0418"], ["j", "\u0419"], ["k", "\u041a"], ["l", "\u041b"], ["m", "\u041c"], ["n", "\u041d"], ["o", "\u041e"], ["p", "\u041f"], ["r", "\u0420"], ["s", "\u0421"], ["t", "\u0422"], ["u", "\u0423"], ["f", "\u0424"], ["``", "\u042a"], ["`", "\u044a", true], ["y", "\u042b"], ["''", "\u042c"], ["'", "\u044c", true], ["x", "\u0425"], ["h", "\u0425"], ["c", "\u0426"], ["kh", "\u0425"], ["ji", "\u0406"], ["jj", "\u0407"], ["w", "\u040e"], ["q", "'"], ["\\", "'"], ["\u017d", "'"], ["je", "\u0404"]];

//[["shch", "\u0429"], ["yo", "\u0401"], ["y\\o", "\u044b\u043e"], ["y\\u", "\u044b\u0443"], ["y\\a", "\u044b\u0430"], ["zh", "\u0416"], ["ch", "\u0427"], ["sh", "\u0428"], ["yu", "\u042e"], ["ya", "\u042f"], ["e'", "\u042d"], ["eh", "\u042d"], ["sj", "\u0429"], ["a", "\u0410"], ["b", "\u0411"], ["v", "\u0412"], ["g", "\u0413"], ["d", "\u0414"], ["e", "\u0415"], ["z", "\u0417"], ["i", "\u0418"], ["j", "\u0419"], ["k", "\u041a"], ["l", "\u041b"], ["m", "\u041c"], ["n", "\u041d"], ["o", "\u041e"], ["p", "\u041f"], ["r", "\u0420"], ["s", "\u0421"], ["t", "\u0422"], ["u", "\u0423"], ["f", "\u0424"], ["``", "\u042a"], ["`", "\u044a", true], ["y", "\u042b"], ["''", "\u042c"], ["'", "\u044c", true], ["x", "\u0425"], ["h", "\u0425"], ["c", "\u0426"], ["kh", "\u0425"], ["ji", "\u0406"], ["jj", "\u0407"], ["w", "\u040e"], ["q", "\u0027"], ["\\", "\u0027"], ["Ž", "\u0027"], ["je", "\u0404"]];


//[["shch","Щ"],["yo","Ё"],["y\o","ыо"],["y\u","ыу"],["y\a","ыа"],["zh","Ж"],["ch","Ч"],["sh","Ш"],["yu","Ю"],["ya","Я"],["e'","Э"],["eh","Э"],["sj","Щ"],["a","А"],["b","Б"],["v","В"],["g","Г"],["d","Д"],["e","Е"],["z","З"],["i","И"],["j","Й"],["k","К"],["l","Л"],["m","М"],["n","Н"],["o","О"],["p","П"],["r","Р"],["s","С"],["t","Т"],["u","У"],["f","Ф"],["``","Ъ"],["`","ъ",true],["y","Ы"],["''","Ь"],["'","ь",true],["x","Х"],["h","Х"],["c","Ц"],["kh","Х"],["ji", "\u0406"],["jj","\u0407"],["w","\u040E"],["q", "'"],["\\", "'"],["Ž", "'"],["je","\u0404"]];


/*[["shch", "\u0429"],["yo", "\u0401"],["zh",
"\u0416"],["ch", "\u0427"],["sh", "\u0428"],["yu", "\u042E"],["ya",
"\u042F"],["e\'", "\u042D"],["eh", "\u042D"],["sj", "\u0429"],["a",
"\u0410"],["b", "\u0411"],["v","\u0412"],["g", "\u0413"],["d",
"\u0414"],["e", "\u0415"],["z", "\u0417"],["i", "\u0418"],["j",
"\u0419"],["k", "\u041A"],["l", "\u041B"],["m", "\u041C"],["n",
"\u041D"],["o", "\u041E"],["p", "\u041F"],["r", "\u0420"],["s",
"\u0421"],["t", "\u0422"],["u", "\u0423"],["f", "\u0424"],["``",
"\u042A"],["`", "\u044A", true],["y","\u042B"],["\'\'", "\u042C"],["\'",
"\u044C", true],["x", "\u0425"],["h", "\u0425"],["c", "\u0426"],["kh",
"\u0425"],["ji", "\u0406"],["jj","\u0407"],["w","\u040E"],["q", "'"],["\\", "'"],["Ž", "'"],["je","\u0404"]];*/



var conversionTable = defaultLatinicaTable;



// for compatibility with bookmarklets
function cyr_translit(src) {
	return to_cyrillic(src);
}

var conversionHash = undefined;
var maxCyrLength = 0;
var maxLatLength = 0;

function getConversionHash() {
	if (conversionHash == undefined) {
		conversionHash = new Array();
		for (var i = 0; i < conversionTable.length; i++) {
			if (!conversionHash[conversionTable[i][0].toUpperCase()]) {
				conversionHash[conversionTable[i][0].toUpperCase()] = conversionTable[i].slice(1);
				maxLatLength = Math.max(maxLatLength, conversionTable[i][0].length);
			}
		}
	}
	return conversionHash;
}


function to_cyrillic(src, output, chunks) {
	if (src == undefined || src == "" || src == null)
		return src;
	if (output == undefined)
		output = new String();

	var hash = getConversionHash();
	
	var location = 0;
	
	while (location < src.length) {
		var len = Math.min(maxLatLength, src.length - location);
		var arr = undefined;
		var sub;
		while (len > 0) {
			sub = src.substr(location, len);
			arr = hash[sub.toUpperCase()];
			if (arr != undefined) 
				break;
			else 
				len--;
		}
		
		// need this for translit on the fly
		if (chunks != undefined)
			chunks[chunks.length] = [sub, arr == undefined ? 0 : arr[0].length];
			
		if (arr == undefined) {
			output += sub;
			location ++;
		}
		else {
			// case analysis
			var newChar = arr[0];
			
			if (sub.toLowerCase() == sub.toUpperCase() && arr.length > 1 && arr[1] && (newChar.toUpperCase() != newChar.toLowerCase())) {
			
				// need translit hash to determine if previous character (and possibly the one before it) 
				// were converted and are in upper case
				
				// set prevDud to true previous is not a translated character or simply a blank
				// set prevCap to true if previous was translated and was upper case

				var prevCh = output.length == 0 ? null : output.substr(output.length - 1, 1);
				var prevDud = !prevCh || !getTranslitString(prevCh);
				var prevCap = (!prevDud && prevCh == prevCh.toUpperCase());

				// sub is caseless but result isn't. case will depend on lookbehind and lookahead
				if (prevDud || !prevCap) {
					output += newChar.toLowerCase();
					prevCap = false;
				}
				else {
					var next = " ";
					if (location + len < src.length)
						next = src.substr(location + len, 1);

					if (next != next.toUpperCase() && next == next.toLowerCase() ) {
						//next is lowercase (and not caseless)
						output += newChar.toLowerCase();
					}
					else if (next == next.toUpperCase() && next != next.toLowerCase() ) {
						// next is uppercase (and not caseless)
						output += newChar.toUpperCase();
					}
					else {
						// next is caseless. output case determined by the case of output[length - 2]
						var pprevCh = output.length == 1 ? null : output.substr(output.length - 2, 1);
						var pprevDud = !pprevCh || !getTranslitString(pprevCh);
						if (!pprevDud && (pprevCh == pprevCh.toUpperCase())) {
							//pre-prev is in upper case. output is also uppercase
							output += newChar.toUpperCase();
						}
						else {
						    output += newChar.toLowerCase();
						}
						
					}
				}
					
			}
			else if ((sub.toLowerCase() == sub.toUpperCase()) && (arr.length < 2 || !arr[1])) {
				// literal treatment of newChar
				output += newChar;
			}
			else if (sub != sub.toLowerCase()) {
				if (newChar.length > 1 && sub != sub.toUpperCase()) {
					// capitalize first letter of newChar
					output += newChar.substr(0, 1).toUpperCase() + newChar.substr(1).toLowerCase();
				}
				else {
					output += newChar.toUpperCase();
				}
			}
			else {
				// sub is lowercase
			    output += newChar.toLowerCase();
			}
			location += len;
		}
	}
	
	return output;
}

// split string on HTML tags, return array containing both the matches and the pieces of string between them, matches always in even positions - since IE does not support this in String.split
function splitHtmlString(string) {
	var re = /<[\/]?[!A-Z][^>]*>/ig;
	var result = new Array();
	var lastIndex = 0;
	var arr = null;
	while ( (arr = re.exec(string)) != null) {
		result[result.length] = string.substring(lastIndex, arr.index);
		result[result.length] = string.substring(arr.index, re.lastIndex);
		lastIndex = re.lastIndex;
	}
	result[result.length] = string.substr(lastIndex);
	
	return result;
}

/* convert cyrillic to translit using to_translit-- similar to from_translit.... */
function to_translit_ext (src, skipHtml) {
	return convertWithSkip(src, skipHtml, to_translit);
}

/* convert translit to cyrillic (using to_cyrillic above) */
function to_cyrillic_ext (src, skipHtml) {
	return convertWithSkip(src, skipHtml, to_cyrillic);
}


function convertWithSkip(src, skipHtml, converter) {
    if (src == "" || src == null)
        return src;
    if (!skipHtml)
        return converter(src);
    else {
        var arr = splitHtmlString(src);
        
        for (var i = 0; i < arr.length; i++) {
            if ( (i % 2) == 0)
                arr[i] = converter(arr[i]);
        }

        return arr.join("");
    }
}

var translitHash = undefined;

function initTranslit() {
	if (translitHash == undefined) {
		translitHash = new Array();

		for (var i = 0; i < conversionTable.length; i++) {
			var ch = conversionTable[i][1];
				
			maxCyrLength = Math.max(maxCyrLength, ch.length);
			// if the translit string is not caseless, convert cyr string to upper case
			// otherwise maintain its case
			if (conversionTable[i][0].toUpperCase() != conversionTable[i][0].toLowerCase())
				ch = ch.toUpperCase();
				
			if (translitHash[ch] == undefined)
				translitHash[ch] = conversionTable[i][0];
				
		}
	}
}


/* convert cyrillic to translit */
function getTranslitString(ch) {
	initTranslit();
		
	var value = translitHash[ch];
	if (value == undefined)
		value = translitHash[ch.toUpperCase()];
	return value;
}

function to_translit(src) {
    /*
	if (src == undefined || src == "" || src == null)
		return src;
	
	
	var output = new String();
	for (var i = 0; i < src.length; i++) {
		var ch = src.substr(i, 1);
		var value = getTranslitString(ch);
		if (value != undefined) {
			if (ch != ch.toUpperCase()) {
				output += value.toLowerCase();
			}
			else {
				prev = i == 0 ? null : src.substr(i - 1, 1);
				next = i == src.length - 1 ? null : src.substr(i + 1, 1);
				if ( value.length == 1 ||
				   (prev && prev == prev.toUpperCase()) ||
				   (next && next == next.toUpperCase())) {
				     // completely capitalize
				     output += value.toUpperCase();
				}
				else {
					 // capitalize first letter
					 output += value.substr(0, 1).toUpperCase() + value.substr(1).toLowerCase();
				}
			}
		}
		else
			output += ch;
	}

	return output;
	*/
	if (src == undefined || src == "" || src == null)
		return src;
	
	//force initTranslit to get the value of maxCyrLength
	getTranslitString("");
	
	var output = new String();
	var loc = 0;
	while (loc < src.length) {
	//for (var i = 0; i < src.length; i++) {
		var len = Math.min(maxCyrLength, src.length - loc);
		var value = undefined;
		var str = undefined;
		
		while (len > 0) {
			str = src.substr(loc, len);
			value = getTranslitString(str);
			if (value == undefined)
				len--;
			else
				break;			
		}
		//var ch = src.substr(i, 1);
		//var value = getTranslitString(ch);
		if (value != undefined) {
			if (str == str.toLowerCase()) {
				output += value.toLowerCase();
			}
			else if (str != str.toUpperCase() && value.length > 1) {
				// neither upper nor lower - upcase first letter and be done
				output += value.substr(0, 1).toUpperCase() + value.substr(1).toLowerCase();
			}
			else {
				// str is all upcase
			
				var prev = loc == 0 ? null : src.substr(loc - value.length, 1);
				var next = loc == src.length - str.length ? null : src.substr(loc + str.length, 1);
				if ( value.length == 1 || str.length > 1 ||
				   (prev && prev == prev.toUpperCase()) ||
				   (next && next == next.toUpperCase())) {
				     // completely capitalize
				     output += value.toUpperCase();
				}
				else {
					 // capitalize first letter
					 output += value.substr(0, 1).toUpperCase() + value.substr(1).toLowerCase();
				}
			}
		}
		else
			output += str;
			
		loc += str.length;
	}

	return output;
	
}

//-- translit on-the-fly -- 

function replaceValue(node, value, stepback) {
	if (stepback == undefined)
		stepback = 0;
		
	if (isExplorer()) {
		var range = document.selection.createRange();
		range.moveStart("character", -stepback);
		range.text = value;
		range.collapse(false);
		range.select();
	}
	else {
		var scrollTop = node.scrollTop;
		var cursorLoc =  node.selectionStart;
		node.value = node.value.substring(0, node.selectionStart - stepback) + value + 
                node.value.substring(node.selectionEnd, node.value.length);
		node.scrollTop = scrollTop;
		node.selectionStart = cursorLoc + value.length - stepback;
		node.selectionEnd = cursorLoc + value.length - stepback;
	}
}


// compare positions
function positionIsEqual(other) {
	if (isExplorer())
		return this.position.isEqual(other.position);
	else
		return this.position == other.position;
  
}

function Position(node) {
  if (node.selectionStart != undefined)
	this.position = node.selectionStart;
  else if (document.selection && document.selection.createRange())
    this.position = document.selection.createRange();
    
  this.isEqual = positionIsEqual;
}

function resetState() {
	this.position = new Position(this.node);
	this.transBuffer = "";
	this.cyrBuffer = "";
}

function StateObject(node) {
	this.node = node;
	this.reset = resetState;
	this.cyrBuffer = "";
	this.transBuffer = "";
	this.position = new Position(node);
}


var stateHash = new Array();

function isExplorer() {
  return (document.selection != undefined && document.selection.createRange().isEqual != undefined);
}

function pressedKey(event) {
  if (isExplorer())
	return event.keyCode;
  else
    return event.which;
}

function translitonkey(event) {
     /*
	if ((event.keyCode == 255 && event.charCode > 0) || event.keyCode == 8) {
		return;
	}
    */
    
    if (event == undefined)
		event = window.event;
    
	var node = null;
	if (event.target)
		node = event.target;
	else if (event.srcElement)
		node = event.srcElement;
		
	
	
	// initialize state
	var state = stateHash[node];
	if (state == null) {
		state = new StateObject(node);
		stateHash[node] = state;
	}
	if ( (pressedKey(event) > 20) && !event.ctrlKey && !event.altKey && !event.metaKey) {

		var c = String.fromCharCode(pressedKey(event));

		// process input
		var result = process_translit(state, c);
		// finish up
		if (c != result.out || result.replace != 0) {
		  if (isExplorer())
			event.returnValue = false;
		  else
		    event.preventDefault();
		  
		  replaceValue(node, result.out, result.replace);
		  
		  state.position = new Position(node);

		}
	}
	
}

function TranslitResult() {
	this.out = "";
	this.replace = 0;
}

function process_translit(state, c) {
	// reset state if position changed
	if (!state.position.isEqual(new Position(state.node)))
		state.reset();
		
	var result = new TranslitResult();
	
	// initial backbuffer. Add to it as characters are converted
	var backbuffer = getBackBuffer(state.node, state.cyrBuffer.length, 2 * maxLatLength);
	var chunks = new Array();
	
	state.transBuffer = state.transBuffer + c

	var str = to_cyrillic(state.transBuffer, backbuffer, chunks);

	// remove backbuffer from output
	str = str.substr(backbuffer.length);
	result.out = str; 
	/* str is now left alone - it has the output matching contents of chunks and 
	   will be used to reinitialize backbuffers, along with chunks and state.transBuffer
	*/
	
	// get the difference between state.cyrBuffer and output
	for (var i = 0; i < Math.min(state.cyrBuffer.length, result.out.length); i++) {
		if (state.cyrBuffer.substr(i, 1) != result.out.substr(i, 1)) {
			result.replace = state.cyrBuffer.length - i;
			result.out = result.out.substr(i);
			break;
		}
	}
	if (result.replace == 0) {
		result.out = result.out.substr(Math.min(state.cyrBuffer.length, result.out.length));
	}
	
	// update state: backbuffer, bufferArray
	if (chunks.length > 0 && chunks[chunks.length - 1] == result.out.substr(result.out.length - 1)) {
		// no convertion took place, reset state
		state.reset();
	}
	else {
		while (state.transBuffer.length > maxLatLength) {
			state.transBuffer = state.transBuffer.substr(chunks[0][0].length);
			// chunks[i][1] evaluates to false if no conversion took place, otherwise holds the length of cyr string
			str = str.substr(chunks[0][1] ? chunks[0][1] : chunks[0][0].length); 
			chunks.shift();
			
			//chunks.shift();
			//str = str.substr(1);
		}
		state.cyrBuffer = str;
	}
	return result;
}

function getBackBuffer(node, offset, count) {
		
	if (isExplorer()) { //.tagName.toUpperCase() == "EDITOR") {
	
		var range = document.selection.createRange();
		range.moveStart("character", -offset);
		var result = range.text.substr(-count);
		if (!result)
			result = "";
			
		return result;

	} else {
		return node.value.substring(0, node.selectionStart - offset).substr(-count);
	}
}

// need this for bookmarklets
function getSelectedNode() {
  if (document.activeElement)
	return document.activeElement;
  else
    if (window.getSelection && window.getSelection() && window.getSelection().rangeCount > 0) {
		var range = window.getSelection().getRangeAt(0);
		if (range.startContainer && range.startContainer.childNodes && range.startContainer.childNodes.length > range.startOffset)
			return range.startContainer.childNodes[range.startOffset]
    }
  return null;
}

function toggleCyrMode() {
	var node = getSelectedNode();
	if (node) {
		if (stateHash[node]) {
			if (removeKeyEventListener(node))
				delete stateHash[node];
		}
		else {
			if (addKeyEventListener(node))
				stateHash[node] = new StateObject(node);
		}
	}
}

function addKeyEventListener(node) {
	if (node.addEventListener)
		node.addEventListener("keypress", translitonkey, false);
	else if (node.attachEvent)
	    node.attachEvent("onkeypress", translitonkey);
	else return false;
	return true;
}
function removeKeyEventListener(node) {
	if (node.removeEventListener)
		node.removeEventListener("keypress", translitonkey, false);
	else if (node.detachEvent)
		node.detachEvent("onkeypress", translitonkey);
	else return false;
	return true;
}

function getSelectedText() {
	if (isExplorer()) {
		return document.selection.createRange().text;
	}
	else {
		var node = getSelectedNode();
		if (node && node.value && node.selectionStart != undefined && node.selectionEnd != undefined)
			return node.value.substring(node.selectionStart, node.selectionEnd);
	}
	return "";
}


function bmkToCyrillic() {
	batchConverter(to_cyrillic_ext);
}
function bmkToTranslit() {
	batchConverter(to_translit_ext);
	
}


function RangeConversionState(range, converter) {
	this.range = range;
	this.convert = converter;
	this.started = false;
	this.finished = false;
	this.toString = function() {
		return "started : " + this.started + ", finished: " + this.finished;
	};
}

function convertRangeNode(node, state) {
	if (state.started && state.finished)
		return;

	if (!state.started && 
		( ( (state.range.startContainer.nodeType == node.TEXT_NODE || 
			 state.range.startContainer.nodeType == node.PROCESSING_INSTRUCTION_NODE || 
			 state.range.startContainer.nodeType == node.COMMENT_NODE	)
		    && node == state.range.startContainer) 
			||
		  ( state.range.startContainer.childNodes && node == state.range.startContainer.childNodes[state.range.startOffset])
		))
		state.started = true;

	if (node.nodeType == node.TEXT_NODE || node.nodeType == node.PROCESSING_INSTRUCTION_NODE || node.nodeType == node.COMMENT_NODE) {
		if (state.started && !state.finished) {
			// convert text
			var start = (node == state.range.startContainer) ? state.range.startOffset : 0;
			var end   = (node == state.range.endContainer) ? state.range.endOffset : node.nodeValue.length;
			var remainder = (node == state.range.endContainer) ? node.nodeValue.length - state.range.endOffset : 0;
			node.nodeValue = 
				node.nodeValue.substring(0, start) +
				state.convert(node.nodeValue.substring(start, end)) +
				node.nodeValue.substr(end);
			
			if (node == state.range.endContainer)
				state.range.setEnd(node, node.nodeValue.length - remainder);
			if (node == state.range.startContainer)
				state.range.setStart(node, start);
		}
	}
	else if (node.childNodes)
		// walk the tree
		for (var i = 0; i < node.childNodes.length; i++) {
			convertRangeNode(node.childNodes[i], state);
			if (state.started && state.finished)
				break;
		}
		
	if (!state.finished && 
		( ((state.range.endContainer.nodeType == node.TEXT_NODE || 
			 state.range.endContainer.nodeType == node.PROCESSING_INSTRUCTION_NODE || 
			 state.range.endContainer.nodeType == node.COMMENT_NODE	)
		     && node == state.range.endContainer) 
			||
		  ( (state.range.endContainer.childNodes.length > 0) && node == state.range.endContainer.childNodes[state.range.endOffset - 1])
		))
		state.finished = true;
		
}

function convertSelection (selection, converter) {
	if (selection == null) return;
	for(var i = 0; i < selection.rangeCount; i++) {
		convertRangeNode(selection.getRangeAt(i).commonAncestorContainer, new RangeConversionState(selection.getRangeAt(i), converter));
	}
	selection.collapseToEnd();
}


function batchConverter(convert) {
	if (isExplorer()) {
		var range = document.selection.createRange();
		try {
			range.pasteHTML(convert(range.htmlText, true));
		}
		catch (err) {
			range.text = convert(range.text, true);
		}
	}
	else if (window.getSelection) {
		var node = getSelectedNode();
		var sel = window.getSelection();

		if (node && node.value && node.selectionStart != undefined && node.selectionEnd != undefined)
			replaceValue(node, convert(node.value.substring(node.selectionStart, node.selectionEnd), true));
		else if(sel && sel.toString() != "")
			convertSelection(sel, convert);
	}
}
