// sudoku

// global vars
var VERSION = "1.0";
var ORIGINATION = "web";
var sudokuDOM = null;

var req = null;
var gameXSL = null;
var userXSL = null;
var errorXML = null;

var gameXSLTProcessor = null;
var userXSLTProcessor = null;
var errorXMLTProcessor = null;

var gameXSLLoaded = false;
var userXSLLoaded = false;
var errorXSLLoaded = false;

var bInMenu = '';
var currentCell = 0;
var kh = null;

var game = null;
var user = null;
var errors = null;
var logDebug = false;

var size;
var numblocks;
var numfields;

// prefs
var showauto = null;
var showincorrect = null;
var autosave = null;

// default keys
DOWN = Event.KEY_DOWN; //40
UP = Event.KEY_UP; //38
LEFT = Event.KEY_LEFT; //37
RIGHT = Event.KEY_RIGHT; //39
NEXT_EMPTY_CELL = 34; // PAGE_DOWN
PREV_EMPTY_CELL = 33; // PAGE_UP
FIRST_CELL = 36; // HOME
LAST_CELL = 35; // END

// start
var http = createRequestObject();
window.onload = init;

function init() {
	// load XSL stylesheets
	errorXSL = document.implementation.createDocument("","test",null);
	errorXSL.onload = function() { errorXSLLoaded = true; };
	errorXSL.load(timeStamp("error.xsl"));
	errorXSLTProcessor = new XSLTProcessor();
	errorXSLTProcessor.importStylesheet(errorXSL);
	
	userXSL = document.implementation.createDocument("","test",null);
	userXSL.onload = function() { userXSLLoaded = true; };
	userXSL.load(timeStamp("user.xsl"));
	userXSLTProcessor = new XSLTProcessor();
	userXSLTProcessor.importStylesheet(userXSL);
	
	gameXSL = document.implementation.createDocument("","test",null);
	gameXSL.onload = function() { gameXSLLoaded = true; };
	gameXSL.load(timeStamp("game.xsl"));
	gameXSLTProcessor = new XSLTProcessor();
	gameXSLTProcessor.importStylesheet(gameXSL);
	
	if (logDebug) removeClass(el('debugXML'),"hidden");
	
	if (sudokuDOM == null) {		
		// try to load user data
		req = createRequestObject();
		req.open('get',timeStamp('load.php?version='+VERSION+'&origination='+ORIGINATION));
		req.onreadystatechange = initComplete;
		req.send(null);
	}
}

function initComplete(request) {
	if(req.readyState == 4){
		user = null;
		game = null;
		errors = null;
        sudokuDOM = req.responseXML;
        
		if (!sudokuDOM) { alert('Unrecoverable error'); return false; }
		
		errors = sudokuDOM.getElementsByTagName("error");
		if (errors.length) {
			showErrors();
			return false;
		}
        el('xmldebugarea').value = req.responseText;
        
		loadUser();
		
		// check if there's a game
		games = sudokuDOM.getElementsByTagName("game");
		if (games.length) {
			size = parseInt(getDOMValue(sudokuDOM,"size"));
			numblocks = size*size;
			numfields = numblocks * numblocks;
			addClass(el('loading'),'hidden');
			redrawGameArea();
		}
    }
}

function showErrors() {
	if (!errorXSLLoaded) {
		setTimeout("showErrors()",150);
		return false;
	}
	addClass(el('loading'),'hidden');
	var errorDoc = document.implementation.createDocument("","test",null);
	var errorHtml = errorXSLTProcessor.transformToFragment(sudokuDOM,errorDoc);
	elem = el('errors');
	while (elem.hasChildren) elem.removeChild(elem.firstChild);
	elem.appendChild(errorHtml);
}

function loadUser() {
	redrawUserArea();
	setupMenuItems();
	addKeyCommands();
}

function redrawUserArea() {
	if (!userXSLLoaded) {
		setTimeout("redrawUserArea()",150);
		return false;
	}
	
	if (!user) user = sudokuDOM.getElementsByTagName("user").item(0);
	
	autosave = getDOMValue(user,"autosave");
	
	if (user) {
		var userDoc = document.implementation.createDocument("","test",null);
		var userHtml = userXSLTProcessor.transformToFragment(user,userDoc);
		elem = el('user');
		while (elem.hasChildren) elem.removeChild(elem.firstChild);
		elem.appendChild(userHtml);
	}
}

function redrawGameArea() {
	if (!gameXSLLoaded) {
		setTimeout("redrawGameArea()",150);
		return false;
	}
	
	if (!game) game = sudokuDOM.getElementsByTagName("game").item(0);
	
	if (game) {
		if (showauto == null || showincorrect == null) {
			showauto = parseInt(getDOMValue(game,"showauto"));
			showincorrect = parseInt(getDOMValue(game,"showincorrect"));
		}
		updateAutoScratches();
		var gameDoc = document.implementation.createDocument("","doc",null);
		var gameHtml = gameXSLTProcessor.transformToFragment(game,gameDoc);
		elem = el('game');
		while (elem.hasChildren) elem.removeChild(elem.firstChild);
		elem.innerHTML = '';
		elem.appendChild(gameHtml);
	}
	
	moveCell(-1,currentCell);
}

function moveCell(oldCell,newCell) {
	if (oldCell < 0) oldCell = currentCell;
	
	obj = document.getElementById('field_'+oldCell);
	removeClass(obj,'selectedCell');
	
	obj = document.getElementById('field_'+newCell);
	addClass(obj,'selectedCell');
}

function moveHandler(event) {
	var rowstart = Math.floor(currentCell/numblocks)*numblocks;
	var rowend = rowstart + numblocks-1;
	var colstart = currentCell%numblocks;
	var colend = colstart + numfields - numblocks;
	
	var oldcell = currentCell;
	
	if (event.keyCode == LEFT && !event.shiftKey) {
		currentCell -= 1;	//left
		if (currentCell < rowstart) currentCell = rowend;
	} else if (event.keyCode == LEFT) currentCell = rowstart;
	if (event.keyCode == UP && !event.shiftKey) {
		currentCell -= numblocks;	//up
		if (currentCell < colstart) currentCell = colend;
	} else if (event.keyCode == UP) currentCell = colstart;
	if (event.keyCode == RIGHT && !event.shiftKey) {
		currentCell += 1;	//right
		if (currentCell > rowend) currentCell = rowstart;
	} else if (event.keyCode == RIGHT) currentCell = rowend;
	if (event.keyCode == DOWN && !event.shiftKey) {
		currentCell += numblocks;	//down
		if (currentCell > colend) currentCell = colstart;
	} else if (event.keyCode == DOWN) currentCell = colend;
	if (event.keyCode == FIRST_CELL) currentCell = 0;
	if (event.keyCode == LAST_CELL) currentCell = numfields-1;
	if (event.keyCode == NEXT_EMPTY_CELL) {
		alert('next');
	}
	if (event.keyCode == PREV_EMPTY_CELL) {
		alert('prev');
	}
	moveCell(oldcell,currentCell);
}

function changeCellValue(newValue) {
	// first check if this cell is changeable
	var cell = el('field_'+currentCell);
	if (cell.firstChild && hasClass(cell.firstChild,"set")) return false;
	
	var cells = game.getElementsByTagName('cell');
	
	cell = cells.getElementsByAttribute("fieldid",currentCell)

	if (getDOMValue(cell,"current") == newValue) setDOMValue(cell, "current", 0);
	else setDOMValue(cell,"current",newValue);

	redrawGameArea();
}

function changeScratchValue(newValue) {
	// first check if this cell is changeable
	var cell = el('field_'+currentCell);
	if (cell.firstChild && hasClass(cell.firstChild,"set")) return false;
	
	// try and change it
	var cells = game.getElementsByTagName('cell');
	var i = 0;
	cell = cells.item(i);
	
	while (cell.getAttribute("fieldid") != currentCell) {
		i++;
		cell = cells.item(i);
	}

	if (!showauto) {
		// check for current existance
		var bSFound = false;
		var scratches = cell.getElementsByTagName('scratch');
		if (scratches) {
			s = scratches.item(0).getElementsByTagName('s');
			if (s) {
				// remove old scratch
				for (i=0; i<s.length; i++) {
					if (parseInt(s.item(i).firstChild.nodeValue) == newValue) {
						s.item(i).parentNode.removeChild(s.item(i));
						bSFound = true;
						break;
					}
				}
			}
			if (!bSFound) {
				newScratch = sudokuDOM.createElement('s');
				textnode = sudokuDOM.createTextNode(newValue);
				newScratch.appendChild(textnode);
				scratches.item(0).appendChild(newScratch);
			}
		}
	} else {
		var bSFound = false;
		var autos = cell.getElementsByTagName('auto');
		if (autos) {
			s = autos.item(0).getElementsByTagName('s');
			if (s) {
				// remove old scratch
				for (i=0; i<s.length; i++) {
					if (s.item(i).firstChild.nodeValue == (newValue + 'x')) {
						s.item(i).parentNode.removeChild(s.item(i));
						bSFound = true;
						break;
					}
				}
			}
			if (!bSFound) {
				newScratch =sudokuDOM.createElement('s');
				textnode = sudokuDOM.createTextNode(newValue + 'x');
				newScratch.appendChild(textnode);
				autos.item(0).appendChild(newScratch);
			}
		}
	}
	redrawGameArea();
}

function addKeyCommands() {	
	var display = {};
	// uses prototype
	kh = new KeyHandler(window,display);
	
	var moveKeys = kh.addHandler({ keys: [DOWN,UP,LEFT,RIGHT,NEXT_EMPTY_CELL,
										     PREV_EMPTY_CELL,FIRST_CELL,LAST_CELL], 
					shiftKey: false,
					active: true,
					action: moveHandler 
					});
					
	var moveKeysShift = kh.addHandler({ keys: [DOWN,UP,LEFT,RIGHT], 
					shiftKey: true,
					active: true,
					action: moveHandler 
					});
					
	var insertNumberKeypadValueKeys = kh.addHandler({
		keys: Array().createRange(97,105),
		active: true,
		action: function(event) { 
			var value = event.keyCode - 96;
			if (value <= numblocks) changeCellValue(value);
			}
		});
		
	var insertNumberValueKeys = kh.addHandler({
		keys: Array().createRange(49,57),
		active: true,
		action: function(event) { 
			var value = event.keyCode - 48;
			if (value <= numblocks) changeCellValue(value);
			}
		});
	
	var insertNumberKeypadScratchKeys = kh.addHandler({
		keys: Array().createRange(97,105),
		shiftKey: true,
		active: true,
		action: function(event) { 
			var value = event.keyCode - 96;
			if (value <= numblocks) changeScratchValue(value);
			}
		});
		
	var insertNumberScratchValueKeys = kh.addHandler({
		keys: Array().createRange(49,57),
		shiftKey: true,
		active: true,
		action: function(event) { 
			var value = event.keyCode - 48;
			if (value <= numblocks) changeScratchValue(value);
			}
		});
	
}

function setupMenuItems() {
	var menu = el('userMenu');
	var menuItems = menu.getElementsByTagName('li');
	var elem = null;
	
	// generic menu item handlers
	for (var i=0;i<menuItems.length;i++) {
		menuItem = menuItems[i];
		
		menuItem.onclick = function() { 
			if (bInMenu == '') {
				var elem = el(this.id + 'FieldSet');
				if (elem) {
					toggleClass(elem,'hidden');
					toggleClass(el('game'),'faded');
					bInMenu = this.id;
				}
			}
		};
		
		// setup cancel buttons
		elem = el(menuItem.id + "FieldSet");
		if (elem) elem = elem.firstChild;
		while (elem) {
			if (hasClass(elem,"cancelButton")) {
				elem.onclick = function () {
					if (bInMenu) {
						var elem = el(bInMenu + "FieldSet");
						if (elem) {
							toggleClass(elem,"hidden");
							toggleClass(el('game'),'faded');
							bInMenu = '';
						}
					}
				}
			}
			elem = elem.nextSibling;
		}
	}
	
	// specific controls
	
	// options
	
	// prefs
	elem = el('savePrefs');
	elem.onclick = function() {
		savePrefs();
		var elem = el(bInMenu + "FieldSet");
		if (elem) {
			toggleClass(elem,"hidden");
			toggleClass(el('game'),'faded');
			bInMenu = '';
		}
	}
	
	// new
	elem = el('newPuzzle');
	elem.onclick = function() {
		loadGame(-1,el('size').value,0,el('userdifficulty').value,el('numhidden').value,el('numguesses').value);
		var elem = el(bInMenu + "FieldSet");
		if (elem) {
			toggleClass(elem,"hidden");
			toggleClass(el('game'),'faded');
			bInMenu = '';
		}
	};
	
	// load
	elem = el('loadPuzzle');
	elem.onclick = function() { 
		loadGame(el('puzzleid').value,0,0,0,0,0);
		var elem = el(bInMenu + "FieldSet");
		if (elem) {
			toggleClass(elem,"hidden");
			toggleClass(el('game'),'faded');
			bInMenu = '';
		}
	 };
	
	// save
	
	// high scores
	
}

function savePrefs() {
	// try to separate users from the rest of the xml
	var elem = sudokuDOM.getElementsByTagName("users").item(0);
	
	// try to save prefs
	var tmpDoc = document.implementation.createDocument("","sudoku",null);
	var tmpRoot = tmpDoc.firstChild;
	tmpRoot.appendChild(tmpDoc.createElement("games"));
	tmpUsers = tmpRoot.appendChild(tmpDoc.importNode(elem,true));
	tmpRoot.appendChild(tmpDoc.createElement("errors"));

	setDOMValue(tmpUsers,"showauto",el('prefsShowauto').checked ? 1 : 0);
	setDOMValue(tmpUsers,"showincorrect",el('prefsShowincorrect').checked ? 1 : 0);	
	setDOMValue(tmpUsers,"autosave",el('autosave').checked ? 1 : 0);
	
	req = createRequestObject();
	req.open('POST',timeStamp('save.php'),true);
	req.onreadystatechange = saveComplete;
	req.send(tmpDoc);
}

function saveGame() {
	// save game
	req = createRequestObject();
	req.open('POST',timeStamp('save.php'),true);
	req.onreadystatechange = saveComplete;
	req.send(sudokuDOM);
}

function saveComplete() {
	if (req.readyState == 4) {
		alert('save complete');
	}
}

function loadGame(id, size, calcdifficulty, userdifficulty, numhidden, numguesses) {
	if (sudokuDOM) {
		if (!confirm("You are currently playing a puzzle. Do you want to abandon this puzzle for a different one?")) return false;
	}
	
	addClass(el('game'),"hidden");
	removeClass(el('loading'),"hidden");
	
	// clear out blocks
	if (game) {
		var elem = game.getElementsByTagName("blocks");
		elem.item(0).parentNode.removeChild(elem.item(0));
	} else {
		var games = game.getElementsByTagName("games");
		games = games.item(0);
		
		game = sudokuDOM.createElement("game");
		game = games.appendChild(game);
	}

	sudokuroot = sudokuDOM.getElementsByTagName("sudoku").item(0);
	sudokuroot.setAttribute("version",VERSION);
	sudokuroot.setAttribute("origination","web");
	setDOMValue(game,"id",parseInt(id));
	setDOMValue(game,"size",parseInt(size));
	setDOMValue(game,"calcdifficulty",parseInt(calcdifficulty));
	setDOMValue(game,"userdifficulty",parseInt(userdifficulty));
	setDOMValue(game,"numhidden",parseInt(numhidden));
	setDOMValue(game,"numguesses",parseInt(numguesses));
	
	// try to load game data
	req = createRequestObject();
	req.open('POST',timeStamp('load.php'),true);
	req.onreadystatechange = loadGameComplete;
	req.send(sudokuDOM);
}

function loadGameComplete(req) {
	if(req.readyState == 4){
		game = null;
		showauto = null;
		showincorrect = null;
		
        sudokuDOM = req.responseXML;
        el('xmldebugarea').value = req.responseText;
 
		size = parseInt(getDOMValue(sudokuDOM,"size"));
		numblocks = size*size;
		numfields = numblocks * numblocks;

		removeClass(el('game'),'hidden');
		addClass(el('loading'),'hidden');
		redrawGameArea();
    }
}

function updateAutoScratches() {
	var i, j, rowstart, colstart, blockstart, digit, exists, index;
	
	var ts = new Date();
	
	// create array with values
	var cells = game.getElementsByTagName("cell");
	
	currentstate = new Array(numfields);
	aAuto = new Array(numfields);
	aAnti = new Array(numfields);
	fieldids = new Array(numfields);
	
	for (i=0;i<cells.length;i++) {
		value = getDOMValue(cells.item(i),"current");
		field = cells.item(i).getAttribute("fieldid");
		fieldids[field] = i;
		currentstate[field] = value;
		
		if (value == 0) {		
			autos = cells.item(i).getElementsByTagName("auto").item(0);
			scratches = autos.getElementsByTagName("s");
			
			aAuto[field] = new Array();
			aAnti[field] = new Array();
			
			if (scratches.length) {
				for (j=0;j<scratches.length;j++) {
					s = scratches.item(j);
					if (s.firstChild.nodeValue.indexOf("x") >= 0) {
						aAnti[field].push(parseInt(s.firstChild.nodeValue));
					}
				}
			}
		
			while (autos.firstChild) autos.removeChild(autos.firstChild);
		}
	}
	
	for (i=0;i<numfields;i++) {
		if (currentstate[i] == 0) {
			rowstart = Math.floor(i/numblocks)*numblocks;
			colstart = i%numblocks;

			blockstart = Math.floor(rowstart/numblocks/size)*numblocks*size + Math.floor(colstart/size)*size;

			for(digit=1;digit<=numblocks;digit++) {
				exists = false;
				for(j=colstart;!exists && j<numfields;j+=numblocks) {
					if (currentstate[j] == digit) {
						exists = true;
					}
				}

				for(j=rowstart;!exists && j<rowstart+numblocks;j++) {
					if (currentstate[j] == digit) {
						exists = true;
					}
				}
				
				for(j=blockstart;!exists && j<blockstart+size;j++) {
					if (currentstate[j] == digit) {
						exists = true;
					}
				}

				for(j=blockstart+numblocks;!exists && j<(blockstart+numblocks+size);j++) {
					if (currentstate[j] == digit) {
						exists = true;
					}
				}

				for(j=blockstart+(numblocks*2);!exists && j<blockstart+(numblocks*2)+size;j++) {
					if (currentstate[j] == digit) {
						exists = true;
					}
				}
			
				indexAnti = aAnti[i].indexOf(digit);
				if (!exists && indexAnti < 0) {
					aAuto[i].push(digit);
				}
			} // all digits
		} // non-committed
	} // all fields
	
	// set autos
	
	for (i=0;i<cells.length;i++) {
		cell = cells.item(i);
		
		if (cell.firstChild.nodeValue == 0) {
			auto = cell.getElementsByTagName("auto").item(0);
			
			if (aAuto[fieldids[i]]) {
				for (j=0;j<aAuto[fieldids[i]].length;j++) {
					s = sudokuDOM.createElement("s");
					textnode = sudokuDOM.createTextNode(aAuto[fieldids[i]][j]);
					s.appendChild(textnode);
					auto.appendChild(s);
				}
			}
			
			if (aAnti[fieldids[i]]) {
				for (j=0;j<aAnti[fieldids[i]].length;j++) {
					s = sudokuDOM.createElement("s");
					textnode = sudokuDOM.createTextNode(aAnti[fieldids[i]][j] + "x");
					s.appendChild(textnode);
					auto.appendChild(s);
				}
			}
		}
	}
}

// --------------
// global methods
// --------------


//  class related
function hasClass(obj,className) {
	if (!obj) return false;
	return new RegExp('\\b' + className + '\\b').test(obj.className);
}

function removeClass(obj,className) {
	if (obj) {
		var re=obj.className.match(' '+className)?' '+className:className;
		obj.className=obj.className.replace(re,'');
	}
}

function addClass(obj,className) {
	if (obj) {
		if (!hasClass(obj,className)) {
			obj.className += obj.className ? " " + className : className;
		}
	}
}

function toggleClass(obj,className) {
	if (hasClass(obj,className)) {
		removeClass(obj,className);
	} else {
		addClass(obj,className);
	} 
}

// dom methods
function getDOMValue(dom,key) {
	try {
		elem = dom.getElementsByTagName(key);
		return elem.item(0).firstChild.nodeValue;
	} catch(e) {
		alert('key not found:' + key);
	}
}

function setDOMValue(dom,key,value) {
	try {
		elem = dom.getElementsByTagName(key);
		if (!elem.length) {
			if (dom.ownerDocument) {
				elem = dom.ownerDocument.createElement(key);
				textnode = dom.ownerDocument.createTextNode(value);
			} else {
				elem = dom.createElement(key);
				textnode = dom.createTextNode(value);
			}
			elem.appendChild(textnode);
			dom.appendChild(elem);
		} else {
			elem.item(0).firstChild.nodeValue = value;
		}
	} catch(e) {
		alert("key not found:" + key);
	}
}

Object.prototype.getElementsByAttribute = function(attr,value) {
	var i = 0;
	var elem = this.item(i);
	
	if (!elem) return false;
	
	while (elem.getAttribute(attr) != value) {
		i++;
		elem = this.item(i);
		if (!elem) return false;
	}
	
	return elem;
};

// ajax
function createRequestObject() {
    var ro;
    var browser = navigator.appName;
    if(browser == "Microsoft Internet Explorer"){
        ro = new ActiveXObject("Microsoft.XMLHTTP");
    }else{
        ro = new XMLHttpRequest();
    }
    return ro;
}

function timeStamp(strUrl) {
	var dtStamp = new Date();
	var strFinalUrl = strUrl;
				
	if ( strFinalUrl.indexOf('?') > -1 ) {
		strFinalUrl += '&ts=';
	} else {
		strFinalUrl += '?ts=';
	}
				
	return strFinalUrl + dtStamp.getTime();
}

function el(str) {
	var elem = document.getElementById(str);
	return elem;
}

function debug(msg) {
	if (logDebug) { return confirm(msg); } else { return true; }
}

function calcDelay(someDate,msg) {
	alert(msg + ":" + (new Date() - someDate));
	return new Date();
}

function myDump(aMessage) {
  var consoleService = Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService);
  consoleService.logStringMessage("My extension: " + aMessage);
}
	