// ==UserScript==
// @name           MathDoku enhancer
// @namespace      www.daddy.sk
// @description    Makes mathdoku.com user friendlier. 
// @include        *www.mathdoku.com/*
// ==/UserScript==

//!! @todo: 
// - Undo/redo: optimize storing/not storing state
// - Remember solved puzzles along with best time
//      - half solved: remembering without time
// - Auto check answers when all cells are finalized
//      - half solved: working, except when using 'start over' button or undo/redo.


debugMode		= true;

CELL_PREFIX      = 'c';
INPUT_PREFIX     = 'n';
HINT_PREFIX      = 'f';
CL_CELL_FOCUS    = '#EEEEAA';
CL_COLLISON      = '#FFAFAF'
CL_CAND_HIGHLIGHT= '#AFFFAF'
CN_FOCUSED = 'focused';
CN_CELL = 'cell';
CN_FINAL = 'final_value';
CN_COLLISION = 'value_collision';
CN_CAND_HIGHLIGHT = 'highlighted';
BLINK_TIMEOUT = 500;
AUTO_FILL_OBVIOUS = true;
TORUSLIKE_MOVEMENT = true;

focEle = null;
finalCount = 0;
coll = false;
altPressed = false;

undoStackClasses = [];
undoStackContents = [];
undoStackPos = 0;

  /////////////////////////////////
 //             General Functions              //
/////////////////////////////////

// Log the message to the console if debugging is turned on (debugMode variable)
function log(msg) {
	if (debugMode) {
		GM_log(msg);
	}	
}  

// Evaluate xPath against the given element or document if no element is given.
function xPath(query, doc) {
    var xResult, arrResult = [], item;
	if (!doc) {
		doc = document;
	}
	xResult = document.evaluate(query, doc, null, null, null);	
	while (item = xResult.iterateNext()) {
		arrResult.push(item);
	}
	return arrResult;
} 

function daysBetween(date1, date2) {
    var ONE_DAY = 1000 * 60 * 60 * 24
    return Math.round(Math.abs(date1.getTime() - date2.getTime())/ONE_DAY)
}

function addClass(ele, c) {
    if (ele.className.indexOf(" " + c) == -1) { 
        ele.className = ele.className + " " + c;
        return 1;
    }
    return 0;
}

function removeClass(ele, c) {
    if (ele.className.indexOf(" " + c) == -1) { 
        return 0;
    } else {
        ele.className = ele.className.replace(" " + c, "");
        return -1;
    }
}

function toggleClass(ele, c) {
    if (ele.className.indexOf(" " + c) == -1) { 
        ele.className = ele.className + " " + c;
        return 1;
    } else {
        return removeClass(ele, c); //!! duplicit class searching
    }
}

  /////////////////////////////////
 //             Field Management           //
/////////////////////////////////

function getParentCell(e) {
    return document.getElementById(CELL_PREFIX + e.id.substring(2,3) + e.id.substring(1,2));
}

function getChildInput(e) {
    return document.getElementById(INPUT_PREFIX + e.id.substring(2,3) + e.id.substring(1,2));
}

function getNumbr(x, y) {
    //log('Getting: ' + INPUT_PREFIX + y + x);
    return document.getElementById(INPUT_PREFIX + y + x);
}

function detectCollision(x, y, orig) {
    n = getNumbr(x, y);
    if ((n.className.indexOf(CN_FINAL) != -1) && ((orig == "") || (orig == n.value))) {
        log("hit: " + x + ", " + y);
        //log("   value: " + n.value);
        //log("   result:");
        //log("       " + result + " ->");
        addClass(n.parentNode, CN_COLLISION);
        coll = true;
        //log("       " + result);
        return n.value;
    }
    return "";
}

// , orig is an ugly 'hack'
function getCandidates(x, y, orig) {
    x *= 1;
    y *= 1;
    var i = 0;
    coll = false;
    result = "12345678".substring(0, size);
    for (i=0; i<size; i++) {
        if (i != x) {
            result = result.replace(detectCollision(i, y, orig), "");
        }
        if (i != y) {
            result = result.replace(detectCollision(x, i, orig), "");
        }        
    }
    
    if (coll) {
        //!! todo: handle existing timeout
        window.setTimeout(clearCellFlags, BLINK_TIMEOUT);
    }
    
    return result;
}

function fillAllCandidates() {
    var i,k;
    
    for (i = 0; i < size; i++) {
        for (k = 0; k < size; k++) {
            n = getNumbr(i,k);
            if (n.value == "") {
                n.value = getCandidates(i, k, "");
            }
        }
    }
}

function highlightCandidate(c) {
    log('ighlightCandidate(' + c + ')');
    var i,k;
    
    for (i = 0; i < size; i++) {
        for (k = 0; k < size; k++) {
            n = getNumbr(i,k);
            if (n.value.indexOf(c) != -1) {
                addClass(n.parentNode, CN_CAND_HIGHLIGHT);
            }
        }
    }
}

function clearCellFlags() {
    log('clearCellFlags');
    var i = 0;
    var k = 0;
    var ele;
    for (i = 0; i < size; i++) {
        for (k = 0; k < size; k++) {
            ele = document.getElementById(CELL_PREFIX + i + k);
            removeClass(ele, CN_COLLISION);
            removeClass(ele, CN_CAND_HIGHLIGHT);
        }
    }
}

function removeCandidate(x, y, c) {
    var ele = getNumbr(x, y);
    if (!ele) {
        log('   error 1: ('+x+', '+y+') not found');
        return -1;
    }
    if (ele.value.length > 1) {
        //log('(x,y) = ('+x + ', ' + y + '); (i,y) = (' + i + ', ' + y + ')');
        //log('   ele.value = ' + ele.value + ' -->' );
        ele.value = ele.value.replace(c, "");
        //log('       ' + ele.value);
        if (AUTO_FILL_OBVIOUS && (ele.value.length == 1)) {
            setFinal(x, y);
        }
    }
    return 1;
}

function setFinal(x, y) {
    x *= 1;
    y *= 1;
    var fin = getNumbr(x, y);
    log("setFinal(" + x +", " + y + ")");
    if ((!fin) || (fin.value.length != 1)) {
        log("   error: " + (fin?"length":"fin"));
        return;
    }
    // Set class as final
    finalCount += addClass(fin, CN_FINAL);
    log('finalCount: ' + finalCount);
    if (finalCount == (size*size)) {    
        log('All solved, checking answers');
        unsafeWindow.checkAnswers();
        log('checkAnswers call finished');
    }
    // Remove value from affected cells
    for (var i = 0; i < size; i++) {
        if (i != x) {
            removeCandidate(i, y, fin.value);
        }
        if (i != y) {
            removeCandidate(x, i, fin.value);
        }
    }
}

function undoFinal(x, y) {
    fin = getNumbr(x, y);
    if (!fin) {
        log("undoFinal(" + x +", " + y + ")");
        log("   error");
        return;
    }
    finalCount += removeClass(fin, CN_FINAL);
    log('finalCount: ' + finalCount);
}

function getEventTarget(e) {
    return e?e.target:window.event.target;
}

function insertSorted(str, ch) {
    var i = 0;
    ins = false;
    for (i=0; i<str.length; i++) {
        if (str[i] > ch) {
            str = str.substring(0,i) + ch + str.substring(i);
            ins = true;
            break;
        }     
    }
    if (!ins) {
        str += ch;
    }
    return str;
}

// Currently only determining whether and when puzzle has been seen before
function checkLocalPuzzleInfo() {
    window.addEventListener(
    	'load',
    	function() {
    		setTimeout(
    			function () {
                  	now = new Date();                        
                    puzzleID = document.getElementById('diff');
                    if (puzzleID && (puzzleID.value != '')) {
                        puzzleIDVal = puzzleID.value;
                        log('puzzleID: ' + puzzleIDVal);
                        localPuzzleInfo = GM_getValue(puzzleIDVal, '0');
                        if (localPuzzleInfo == '0') {
                            log('   first time seen');
                            puzzleID.value += ': new';
                        } else {
                            log('   already seen');
                            puzzleID.value += ': ' + daysBetween(now, new Date(localPuzzleInfo)) + ' days ago';
                        }
                        GM_setValue(puzzleIDVal, String(now));
                    } else {
                        log('getting diff failed...');
                    }
    			},
    		200);
    	},
      false
    );
}
  /////////////////////////////////
 //             Undo Functionality             //
/////////////////////////////////

function storeStatus() {
    log('<storeStatus()>');
    undoStackClasses[undoStackPos] = ['','','','','','','','']; //!! Disgusting!
    undoStackContents[undoStackPos] = ['','','','','','','',''];
    for (i=0; i<size; i++) {
        undoStackClasses[undoStackPos][i] = ['','','','','','','',''];
        undoStackContents[undoStackPos][i] = ['','','','','','','',''];
        for (k=0; k<size; k++) {
            numbr = getNumbr(i, k);
            if (numbr) {
                undoStackClasses[undoStackPos][i][k] = numbr.className;
                undoStackContents[undoStackPos][i][k] = numbr.value;
            } else {
                log('No numbr!');
            }
        }
    }
    undoStackPos++;
    log('</storeStatus()>');
}

function resumeStatus(n) {
    log('<resumeStatus()>');
    if ((n >= undoStackClasses.length) || ( n < 0 )) {
        log('</resumeStatus() - err>');
    }
    for (i=0; i<size; i++) {
        for (k=0; k<size; k++) {
            numbr = getNumbr(i, k);
            if (numbr) {
                numbr.className = undoStackClasses[n][i][k];
                numbr.value = undoStackContents[n][i][k];
            }
        }
    }
    log('</resumeStatus()>');
}

function undo() {
    if (undoStackPos <= 0) {
        return;
    }
    //if (undoStackPos = undoStackClasses.length) {
        storeStatus();
        undoStackPos--;
    //}
    resumeStatus(--undoStackPos);
}

function redo() {
    if (undoStackPos >= undoStackClasses.length) {
        return;
    }
    resumeStatus(undoStackPos);
    undoStackPos++;
}

  /////////////////////////////////
 //             Event Handlers               //
/////////////////////////////////
 
function onMouseOver(e) {
    ele = getEventTarget(e);
    //log('onmouseover ' + ele.id);
    switch (ele.id.substring(0,1)) {
        case CELL_PREFIX:        
            getChildInput(ele).focus();
            break;
        case INPUT_PREFIX:
            ele.focus();
        default:
    }
    //log('Focused: ' + e.id);
}

function onMouseOut(e) {
    ele = getEventTarget(e);
    //log('onMouseOuted: ' + e.id);
}

function onBlur(e) {
    ele = getEventTarget(e);
    //log('onBlur: ' + e.id);
    p = getParentCell(ele);
    removeClass(p, CN_FOCUSED);    
}

function onMouseClick(e) {
    ele = getEventTarget(e);
    log("Clicked: " + ele.id);
    if (e.ctrlKey) {
        log("   with crtl");
        setFinal(ele.id.substring(2,3), ele.id.substring(1,2));
    }
}

function onFocus(e) {
    ele = getEventTarget(e);
    p = getParentCell(ele);
    
    if (focEle) {
        removeClass(focEle, CN_FOCUSED);
    }
    
    addClass(p, CN_FOCUSED);
    focEle = p;
}

function onKeyPress(e) {
    var ele = getEventTarget(e);
    
    if (e.altKey) {
        return; // Currently, the only used alt combinatios are handled in keydown and keyup events
    }
    
    var x = ele.id.substring(2,3) * 1;
    var y = ele.id.substring(1,2) * 1;
    var moving = true;
    var keychar = String.fromCharCode(e.charCode);
    log('keynum: ' + e.keyCode + ' which: ' + e.charCode + ' keychar: ' + keychar + ' alt: ' + e.altKey + ' ctrl: ' + e.ctrlKey);
    
    if ((keychar == 'z') && (e.ctrlKey)){
        undo();
        return;
    }
    
    if ((keychar == 'y') && (e.ctrlKey)){
        redo();
        return;
    }
    
    // movement by arrows
    switch (e.keyCode) { // 37- left 38 - up 39 - right 40 - down
        case 38:
            y -= 1;
            break;
        case 40:
            y += 1;
            break;
        case 37:
            x -= 1;
            break;
        case 39:
            x += 1;
            break;
        default:
            moving = false;
    }
    if (moving) {
        if (x < 0) { x = (TORUSLIKE_MOVEMENT?x+size:0); }
        if (x >= size) { x = (TORUSLIKE_MOVEMENT?x-size:size-1); }
        if (y < 0) { y = (TORUSLIKE_MOVEMENT?y+size:0); }
        if (y >= size) { y = (TORUSLIKE_MOVEMENT?y-size:size-1); }
        
        getNumbr(x, y).focus();
        return;
    }
    
    storeStatus();
    undoStackClasses.length = undoStackPos;
    undoStackContents.length = undoStackPos;

    //log('Status: ' + undoStackContents[undoStackPos-1]);
    // Auto fill candidates or set candidate as final
    if ((e.keyCode == 13) || (e.charCode == 32)) {
        if (e.ctrlKey) { // Fill candidates in ALL cells if ctrl pressed
            fillAllCandidates();
            //return;            
        }
        if (ele.value == "") {
            ele.value = getCandidates(x, y, "");
        } else {
            setFinal(x, y);
            //return;
        }
    }
        
    // Delete content
    if ((e.keyCode == 46) || (e.keyCode == 8) || (keychar == 'c') || (keychar == 'C')) {
        ele.value = '';
    }
    
    //!! todo: replace getCandidates with dedicated function (and remove clearCellFlags call) so only affected cells blink
    if ((e.charCode >= 49) && (e.charCode < 49 + size) && (getCandidates(x, y, keychar).indexOf(keychar) != -1)) {
        clearCellFlags();
        if (e.ctrlKey) {
            ele.value = keychar;
            setFinal(x, y);
            return;
        } else {
            if (ele.value.indexOf(keychar) > -1) {
                ele.value = ele.value.replace(keychar,'');
            } else {
                ele.value = insertSorted(ele.value, keychar);
            }
        }
    }
    
    if (ele.value.length != 1) {
        log('ele.id: ' + ele.id);
        log('ele.value: ' + ele.value);
        log('   length: ' + ele.value.length);
        undoFinal(x, y);
    }
}

function onKeyUp(e) {
    if (altPressed) {
        altPressed = false;
        window.setTimeout(clearCellFlags, BLINK_TIMEOUT);
        log('Scheduling clearCellFlags');
    }
}

function onKeyDown(e) {
    //log('onKeyDown keynum: ' + e.keyCode);
    if (altPressed) {
        //log("   alt pressed. Charcode: " + e.charCode );
        if ((e.keyCode >= 49) && (e.keyCode < 49 + size)) {
            var keychar = String.fromCharCode(e.keyCode);
            //log("Alt + " + keychar);
            highlightCandidate(keychar);
        }
    } else {
        altPressed = e.altKey;
        //log("Alt down");
    }
}

  /////////////////////////////////
 //              Initialization                //
/////////////////////////////////

// Clean up gui
document.getElementById('maincontainer').innerHTML = document.getElementById('leftcolumn').innerHTML;
document.getElementById('frm1').parentNode.style.paddingTop = '10px';
GM_addStyle("div.sociable { display: none } div.np { display: none } #maincontainer { padding:0px }");
top.document.title = 'MathDoku';

// Custom styles
GM_addStyle("." + CN_FOCUSED + " { background-color: " + CL_CELL_FOCUS + " }");
GM_addStyle("." + CN_COLLISION + " { background-color: " + CL_COLLISON + " }");
GM_addStyle("." + CN_CAND_HIGHLIGHT + " { background-color: " + CL_CAND_HIGHLIGHT + " }");
GM_addStyle("." + CN_FINAL + " { /*color: #55DD55 !important; */font-size:18pt !important; font-weight: bold; }");
GM_addStyle("td { height: 50pt !important; width: 50pt !important; }");
GM_addStyle(".guessR { background-color:#AAFFAA; }");
GM_addStyle(".d0 { font-size:11pt; height: 30pt; vertical-align: bottom; color:#5555FF; }");

// Get the rows of the puzzle and determine its size
//rows = xPath('//table[@id="tbl1"]/tbody/tr');

numbrs = xPath('//input[@name="numbr"]');
log('numbrs: ' + numbrs.length);
size = Math.sqrt(numbrs.length);
log('Size: ' + size);

checkLocalPuzzleInfo();

for (var i = 0; i < numbrs.length; i++) {
    numbr = numbrs[i];
    
    // Set the max length of the inputs to accomodate at least n-1 numbers
    numbr.setAttribute('maxlength', size-1);
    p = getParentCell(numbr);
    
    // Add mouse & keyboard events listeners
    p.addEventListener("mouseover", onMouseOver, true);
    p.addEventListener("click", onMouseClick, true);
    numbr.addEventListener("mouseout", onMouseOut, true);
    numbr.addEventListener("blur", onBlur, true);
    numbr.addEventListener("focus", onFocus, true);
    numbr.addEventListener("keypress", onKeyPress, true);
    numbr.addEventListener("keydown", onKeyDown, true);
    numbr.addEventListener("keyup", onKeyUp, true);
    numbr.setAttribute('readonly', 'readonly');
}  

