
var puzzles = {};
var __puzzle_counter = 0;

function stringToMatrix (string, size) {
  matrix = [];
  for (var col = 0; col < size; col++) {
    matrix.push([]);
    for (var row = 0; row < size; row++) {
      matrix[col].push(string.substr(row*size+col, 1));
    }
  }
  return matrix;
}

function allPositions(size) {
  var positions = [];
  for (var i = 0; i < size; i++)
    for (var k = 0; k < size; k++)
      positions.push([i, k]);
  return positions;
}

// Cache function results
var __relatedPositionsCache = {};
function getRelatedPositions(x, y, size) {
  var mhash = String(x)+':'+String(y)+':'+String(size);
  var t = __relatedPositionsCache[mhash];
  if(!t) {
    __relatedPositionsCache[mhash] = getRelatedPositions_(x, y, size);
    t = __relatedPositionsCache[mhash];
  }
  return t;
}

function getRelatedPositions_(x, y, size) {
  var positions = [];
  var sqrt = Math.sqrt(size);
  for (px = 0; px < size; px++)
    positions.push([px, y]);
  for (py = 0; py < size; py++)
    positions.push([x, py]);
  
  var bx = Math.floor(x / sqrt);
  var by = Math.floor(y / sqrt);
  var block_positions = [];
  for (var i = 0; i < sqrt; i++)
    for (px = bx*sqrt; px < bx*sqrt+sqrt; px++)
      positions.push([px, (by*sqrt+i)]);
  // Remove all duplicate coordinates
  positions = $.unique(positions);
  return positions;
}


function getPossibilityMatrix(p, updatePositions) {
  if (!p) return null;

  var size = p['size'];
  var sqrt = Math.sqrt(size);

  // Initialize matrix
  var matrix = [];
  for (var r = 0; r < size; r++) {
    var row = [];
    for (var c = 0; c < size; c++)
      row.push([false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 
		false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]);
    matrix.push(row);
  }

  var state = p['state'];

  if (updatePositions == null)
    updatePositions = allPositions(size);

  for (i in updatePositions) {
    var row = updatePositions[i][1];
    var col = updatePositions[i][0];
    var char = state.substr(row*size+col, 1);
    if (char != ' ')
      continue;
    
    var positions = getRelatedPositions(col, row, size);
    var items = '';
    for (pos in positions) {
      var t = state.substr(positions[pos][1]*size+positions[pos][0], 1);
      if (t != ' ')
	items += t;
    }

    items = $.unique(makeArrayFromString(items));
    var possibilities = [];
    var alphabet = makeArrayFromString(p['alphabet']);
    for (var c = 0; c < alphabet.length; c++) {
      if ($.inArray(alphabet[c], items) == -1)
	matrix[row][col][c] = true;
    }
  }
  return matrix;
}

function makeArrayFromString(s) {
  var a = [];
  for(var i = 0; i < s.length; i++)
    a.push(s.substr(i, 1));
  return a;
}

function clone(what) {
    for (i in what) {
        this[i] = what[i];
    }
}

function changeColorTheme (p, theme) {
    if (!p) return null;
    $('#'+p['id']+' .sudoku').removeClass(p['colortheme']).addClass(theme);
    p['colortheme'] = theme;
}

function changeViewsize (p, vs) {
    if (!p) return null;
    $('#'+p['id']+' td.'+p['viewsize']).removeClass(p['viewsize']).addClass(vs);
    p['viewsize'] = vs;
}

drawPuzzle = function (p, settings) {
  if (!(p && puzzles[p['id']]))
    return;

    if ($.browser.msie && p['size'] != 4) {
	var obj = new clone(p);
	obj['history_length'] = p['history'].length;
	obj['history'] = null;
	obj['matrix'] = null;
	$.post('/sudoku/draw.php', obj, function (data) {
	    jQuery('#'+p['id']).html(data)
	    jQuery('#'+p['id']+' input').attachInputHandlers(p);
	})
    } else {

	var entry;
	var html = '<table class="sudoku-parent">';
	
	if (!p['minimal'])
	    html += '<tr><td><table cellspacing="0" cellpadding="0" style="width: 100%;"><tr>' +
	    '<td><table align="left" cellspacing="0" cellpadding="0"><tr>' +
	    '<td class="button '+(p['history_pointer'] || (p['history_pointer'] == null && p['history'].length) ? '' : 'disabled')+'" style="padding-right: 0px;" id="undoButton'+p['id']+'" onclick="undoPuzzle(puzzles[\''+p['id']+'\']);">undo</td>' +
            '<td style="font-weight: bold; width: 3px;"><big>/</big></td>' +
            '<td class="button  '+(p['history_pointer'] != null && p['history_pointer'] != p['history'].length ? '' : 'disabled')+'" style="padding-left: 0px;" id="redoButton'+p['id']+'" onclick="redoPuzzle(puzzles[\''+p['id']+'\']);">redo</td>' +
	    '</tr></table></td>' +
	    '<td><table align="right" cellspacing="0" cellpadding="0"><tr>' +
            '<td class="button" id="checkAnswers'+p['id']+'" title="Check to see if current answers are correct." onclick="alert($.sudoku.isCorrectSoFar(puzzles[\''+p['id']+'\']) ? \'Looking good!\' : \'Uh oh.  Something about this puzzle isn\\\'t right.\');">check</td>' +
            '<td class="button" id="mark'+p['id']+'" title="Place pencil markings on puzzle" onclick="'+(p['mark']?'un':'')+'mark(puzzles[\''+p['id']+'\']);">'+(p['mark']?'un':'')+'mark</td>' +
            '<td class="button" id="solve'+p['id']+'" title="Solve the puzzle" onclick="'+(p['solved']?'un':'')+'solvePuzzle(puzzles[\''+p['id']+'\']);">'+(p['solved']?'un':'')+'solve</td>' +
	    '</tr></table></td>' +
	    '</tr></table></td></tr>';

	if (p['mark']) {
	    var pMatrix = getPossibilityMatrix(p);
	}
	
	var size = p['size'];
	var sqrt = p['sqrt'];
	var viewsize = p['viewsize'];
	var alphabetArray = makeArrayFromString(p['alphabet']);
	var mark = p['mark'];
	var state = p['state'];
	var matrix = p['matrix'];
	var id = p['id'];
	var noinput = p['noinput'];

	html += '<tr><td><table align="center" class="sudoku '+p['colortheme']+'">';
	for (var row = 0; row < size; row++) {
	    html += '<tr>';
	    for (var col = 0; col < size; col++) {
		var tdclasses = viewsize;
		if (row != 0 && row % sqrt == 0) tdclasses += ' border-top';
		if (col != 0 && col % sqrt == 0) tdclasses += ' border-left';
		tdclasses += ' b'+(getBlockNumber(col, row, sqrt) % 7);
		entry = matrix[col][row];
		var char = state.substr(size*row+col, 1);
		if (mark && entry == ' ') {
		    if (char != ' ') {
			entry = '<input type="text" maxlength="1" size="1" value="'+(char==' '?'':char)+'">';
		    } else {
			entry = '<table class="marks" cellspacing="0" cellpadding="0">';
			for (var i = 0; i < sqrt; i++) {
			    entry += '<tr>';
			    for (var k = 0; k < sqrt; k++) {
				if (pMatrix[row][col][i*sqrt+k])
				    entry += '<td onclick="markClicked(puzzles[\''+id+'\'], this);">'+alphabetArray[i*sqrt+k]+'</td>';
				else
				    entry += '<td></td>';
			    }
			    entry += '</tr>';
			}
			entry += '</table>';
		    }
		} else if (entry == ' ' && !noinput) {
		    entry = '<input type="text" maxlength="1" size="1" value="'+(char==' '?'':char)+'">';
		}
		html += '<td id="'+id+'_'+row+'_'+col+'" row="'+row+'" col="'+col+'" class="'+tdclasses+'">' + entry + '</td>';
	    }
	    html += '</tr>';
	}
	html += '</table></td></tr>';

	if (!p['minimal'])
	    html += '<tr><td><table cellspacing="0" cellpadding="0" style="width: 100%;"><tr>' +
	    '<td><table align="left" cellspacing="0" cellpadding="0"><tr>' +
	    '<td class="button" id="clearButton'+id+'" title="Clear puzzle" onclick="'+(p['cleared']?'un':'')+'clearPuzzle(puzzles[\''+id+'\']);">'+(p['cleared']?'un':'')+'clear</td>' +
	    '</tr></table></td>' +
	    '<td><table align="right" cellspacing="0" cellpadding="0"><tr>' +
            '<td class="button" id="link'+id+'" title="Link to this puzzle" onclick="linkPuzzle(puzzles[\''+p['id']+'\']);">link</td>' +
            '<td class="button" id="print'+id+'" title="Print this puzzle" onclick="printPuzzle(puzzles[\''+p['id']+'\']);">print</td>' +
	    '</tr></table></td>' +
	    '</tr></table></td></tr>';

	html += '</table>';
	
	jQuery('#'+id).html(html)
	jQuery('#'+id+' input').attachInputHandlers(p);
	jQuery('#'+id).noselect();
    }
}

jQuery.fn.attachInputHandlers = function (p) {
  return this.each(function () {
      t = jQuery(this);
      t.keydown(function (e) {
	  var event = e ? e : window.event;
	  var value = "";
	  // Letters
	  if (e.which >= 65 && e.which <= 90)
	    value = String.fromCharCode(e.which).toUpperCase();
	  // Number keys (and pad)
	  else if ((e.which >= 49 && e.which <= 57) || (e.which >= 97 && e.which <= 105)) 
	    value = String.fromCharCode(e.which >= 97 ? e.which - 48: e.which);
	  // Control keys
	  else if ((e.which >= 16 && e.which <= 20) || (e.which >= 33 && e.which <= 40) || (e.which >= 91 && e.which <= 93) || (e.which >= 112 && e.which <= 123))
	    return true;
	  else {
	    switch (e.which) {
	      // More control keys
	    case 8: case 9: case 13: case 27: case 45: case 46: case 144: case 145: return true;
	    }
	  }
	  if (value && p['alphabet'].search(value) != -1) {
	    this.value = value;
	    return true;
	  }
	  return false;
	});
      t.keyup(function (e) {
	  var t = jQuery(this);
	  var td = t.parent();
	  var col = parseInt(td.attr("col"));
	  var row = parseInt(td.attr("row"));
	  changePositionValue(p, col, row, this.value);
	  if (p['cleared']) {
	    p['cleared'] = false;
	    drawPuzzle(p);
	  }
	});
      t.blur(function (e) {
	  if (p['mark'] && !this.value || this.value == ' ') {
	    drawPuzzle(p);
	  }
	});
    });
};

function compareArrays (arr1, arr2) {
    if (arr1.length != arr2.length) return false;
    for (var i = 0; i < arr2.length; i++) {
        if (arr1[i] !== arr2[i]) return false;
    }
    return true;
}

function updateMarks (p, positions, oldmatrix) {
    var matrix = getPossibilityMatrix(p, positions);
    var sqrt = p['sqrt'];
    var size = parseInt(p['size']);
    var alphabetArray = makeArrayFromString(p['alphabet']);
    var state = p['state'];
    var string = p['string'];
    var id = p['id'];
    
    for (var pos = 0; pos < positions.length; pos++) {
	var row = positions[pos][1];
	var col = positions[pos][0];
	var char = state.substr(row*size+col, 1);
	var input = false;
	
	if (char != ' ') {
	    
	    if (string.substr(row*size+col, 1) != ' ')
		continue;
	    else {
		html = '<input type="text" maxlength="1" size="1" value="'+(char==' '?'':char)+'">';
		input = true;
	    }
	    
	} else {
	    if (oldmatrix && compareArrays(oldmatrix[row][col], matrix[row][col]))
		continue;
	    html = '<table class="marks" cellspacing="0" cellpadding="0">';
	    for (var i = 0; i < sqrt; i++) {
		html += '<tr>';
		for (var k = 0; k < sqrt; k++) {
		    if (matrix[row][col][i*sqrt+k])
			html += '<td onclick="markClicked(puzzles[\''+id+'\'], this);">'+alphabetArray[i*sqrt+k]+'</td>';
		    else
			html += '<td></td>';
		}
		html += '</tr>';
	    }
	    html += '</table>';
	    
	}
	
	jQuery('#'+id+'_'+row+'_'+col).html(html);
	if (input)
	    jQuery('#'+id+'_'+row+'_'+col+' input').attachInputHandlers(p);
    }
}

function markClicked (p, obj) {
    var t = $(obj);
    var table = t.parent().parent().parent();
    var td = table.parent();
    var r = td.attr('row');
    var c = td.attr('col');
    var v = t.html();
    var size = p['size'];
    if (v != '') {
	var positions = getRelatedPositions(c, r, size);
	if (parseInt(p['size']) >= 16)
	    oldmatrix = getPossibilityMatrix(p, positions);
	else
	    oldmatrix = null;
	changePositionValue(p, c, r, v);
	p['cleared'] = false;
	updateMarks(p, positions, oldmatrix);
	//drawPuzzle(p);
    }
}

function changePositionValue (p, col, row, value) {
    var state = p['state'];
    var size = parseInt(p['size']);
    var pos = parseInt(row*size + parseInt(col));
    var previous_value = state.substr(pos, 1);
    state = state.substr(0, pos) + (!value ? ' ' : value) + state.substr(pos+1, size*size - pos - 1);
    if (state != p['state']) {
	if (p['history_pointer'] != null && p['history_pointer'] < p['history'].length - 1) {
	    var pointer = p['history_pointer'];
	    var newHistory = [];
	    for (var i = 0; i < pointer; i++)
		newHistory.push(p['history'][i]);
	    p['history'] = newHistory;
	    p['history_pointer'] = null;
	}
	p['history'].push({'y': row, 'x': col, 'v': (!value ? '' : value), 'pv': previous_value});
	p['state'] = state;
	if (p['onchange']) eval(p['onchange']);
	if (!p['minimal']) {
	    $('#undoButton'+p['id']).removeClass('disabled'); 
	    $('#redoButton'+p['id']).addClass('disabled');
	    p['canundo'] = true;
	    p['canredo'] = false;
	}
    }
}

function redoPuzzle(p) {
    if (p['history_pointer'] != p['history'].length) {
	redo(p);
	$('#undoButton'+p['id']).removeClass('disabled');
	p['canundo'] = true;
	if (p['history_pointer'] == p['history'].length) {
	    $('#redoButton'+p['id']).addClass('disabled');
	    p['canredo'] = false;
	}
    }
}

function undoPuzzle(p) {
    if (p['history_pointer'] != 0) {
	undo(p);
	$('#redoButton'+p['id']).removeClass('disabled');
	p['canredo'] = true;
	if (p['history_pointer'] == 0) {
	    $('#undoButton'+p['id']).addClass('disabled');
	    p['canundo'] = false;
	}
    }
}

function undo(p) {
    if (!p || !p['history']) return;
    var hp = p['history_pointer'] != null ? p['history_pointer'] : p['history'].length;
    if (hp > 0)
	hp--;
    undoMove = p['history'][hp];
    
    var state = p['state'];
    var size = p['size'];
    var y = parseInt(undoMove['y']);
    var x = parseInt(undoMove['x']);
    var pos = y * size + x;
    var pv = undoMove['pv'];
    
    var cv = state.substr(0, pos, 1);
    state = state.substr(0, pos) + (!pv ? ' ' : pv) + state.substr(pos+1, size*size - pos - 1);
    p['state'] = state;
    
    if (p['mark']) {
	var positions = getRelatedPositions(x, y, size);
	updateMarks(p, positions);
    } else {
	$('#'+p['id']+'_'+y+'_'+x+' input').val(pv);
    }

    p['history_pointer'] = hp;
    //drawPuzzle(p);
}

function redo(p) {
    if (!p || !p['history']) return;
    var hp = p['history_pointer'] != null ? p['history_pointer'] : 0;
    redoMove = p['history'][hp];
    
    var state = p['state'];
    var size = p['size'];
    var y = parseInt(redoMove['y']);
    var x = parseInt(redoMove['x']);
    var pos = y * size + x;
    var v = redoMove['v'];
    
    var cv = state.substr(0, pos, 1);
    state = state.substr(0, pos) + (!v ? ' ' : v) + state.substr(pos+1, size*size - pos - 1);
    p['state'] = state;
    if (hp < p['history'].length)
	hp++;
    p['history_pointer'] = hp;

    if (p['mark']) {
	var positions = getRelatedPositions(x, y, size);
	updateMarks(p, positions);
    } else {
	$('#'+p['id']+'_'+y+'_'+x+' input').val(v);
    }

    //drawPuzzle(p);
}



jQuery.fn.createPuzzle = function (settings) {
  
  settings = jQuery.extend({
      'colortheme': 'default',
	'onchange': '',
	'viewsize': 'big',
	'mark': false,
	'noinput': false,
	'minimal': false
	}, settings);
  
  return this.each (function () {
      
      var t = jQuery(this);
      
      // Mandatory
      var size = t.attr('size');
      var puzzle_id = t.attr('puzzle_id');
      var string = t.attr('string');
      
      if (!size || !puzzle_id || !string)
	return;
      
      // Optional
      var alphabet = t.attr('alphabet');
      if (!alphabet)
	alphabet = (size == 4 ? '1234' :
		    (size == 9 ? '123456789' :
		     (size == 16 ? '123456789ABCDEFG' :
		      (size == 25 ? '123456789ABCDEFGHIJKLMNOP' :
		       (size == 36 ? '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' : null)))));
      var state = t.attr('state');
      if (!state)
	state = string;
      var difficulty = t.attr('difficulty');
      if (!difficulty)
	difficulty = parseInt(puzzle_id.substr(-3, 1));
      var colortheme = t.attr('colortheme');
      if (!colortheme)
	colortheme = settings['colortheme'];
      var viewsize = t.attr('viewsize');
      if (!viewsize)
	viewsize = settings['viewsize'];
      viewsize = viewsize.toLowerCase();
      var minimal = t.attr('minimal');
      minimal = !minimal ? settings['minimal'] : (minimal.toLowerCase() == 'true' || minimal.toLowerCase() == 'yes' ? true : false);
      var mark = t.attr('mark');
      mark = !mark ? settings['mark'] : (mark.toLowerCase() == 'true' || mark.toLowerCase() == 'yes' ? true : false);
      var onchange = t.attr('onchange');
      if (!onchange)
	onchange = settings['onchange'];
      var noinput = t.attr('noinput');
      if (!noinput)
	noinput = settings['noinput'];
      else
	noinput = (noinput.toLowerCase() == 'true' || noinput.toLowerCase() == 'yes' ? true : false);
      var solution = t.attr('solution');
      if (!solution)
	solution = null;

      if (!this.id) {
	this.id = 'puz_' + String(__puzzle_counter);
	__puzzle_counter++;
      }
	
      var matrix = stringToMatrix(string, size);
      var sqrt = Math.sqrt(size);
	
      var p = {
	'id' : this.id,
	'alphabet' : alphabet,
	'size': size,
	'viewsize': viewsize,
	'sqrt': sqrt,
	'puzzle_id': parseInt(puzzle_id),
	'difficulty': difficulty,
	'string': string,
	'state': state,
	'matrix': matrix,
	'minimal': minimal,
	'mark': mark,
	'start': new Date(),
	'colortheme': colortheme,
	'solution': solution,
	'noinput': noinput,
	'history': [],
	'history_pointer': null,
	'onchange': onchange,
	'solved': false,
	'cleared': false,
	'oldstate': null
      };

      puzzles[this.id] = p;

      drawPuzzle(p);
    });
};


// Accepts two puzzle strings.  If s1 and s2 exactly match, return 0.  If
// s1 and s2 have different entries for the same cell, return null (no
// match).  If s1 has less filled in cells than s2, return a number less
// than 0.  If s1 has more filled in cells than s2, return a number
// greater than 0.
var compare = function (s1, s2) {
  if (s1 == s2) return 0;
  if (s1.length != s2.length) return null;
  var count = 0;
  var c1 = null;
  var c2 = null;
  for (var c = 0; c < s1.length; c++) {
    c1 = s1.substr(c, 1);
    c2 = s2.substr(c, 1);
    if (c1 != ' ' && c2 != ' ' && c1 != c2) return null;
    if (c1 == ' ') count++;
    if (c2 == ' ') count--;
  }
  return count;
}


function mark (p) {
  if (!p['mark']) {
    p['mark'] = true;
    drawPuzzle(p);
  }
}


function unmark (p) {
  if (p['mark']) {
    p['mark'] = false;
    drawPuzzle(p);
  }
}


var isCorrectSoFar = function (p) {
  if (!p) return null;

  wait();
  var result = false;
  
  // If we don't have solution, get it
  if (p['solution'] == null) {
    var data = jQuery.ajax({url: "/solve/?id="+p['puzzle_id'], 
	  type: "GET", 
	  async: false}).responseText;
    var json = eval ("("+data+")");
    p['solution'] = json['solution'];
  }
  result = jQuery.sudoku.compare(p['solution'], p['state']);
  nowait();
  return result != null && result <= 0;
};


function wait () {
  $(document.body).css('cursor', 'wait');
}

function nowait() {
  $(document.body).css('cursor', '');
}

function clearPuzzle(p) {
  wait();
  if (p['solved'])
    unsolvePuzzle(p);
  p['oldstate'] = p['state'];
  p['state'] = p['string'];
  p['cleared'] = true;
  drawPuzzle(p);
  nowait();
}

function unclearPuzzle(p) {
  wait();
  p['state'] = p['oldstate'];
  p['oldstate'] = null;
  p['cleared'] = false;
  drawPuzzle(p);
  nowait();
}

function solvePuzzle(p) {
    wait();
    solve(p);
    $('#undoButton'+p['id']).add('#redoButton'+p['id']).addClass('disabled');
    p['canundo'] = p['canredo'] = false;
    nowait();
}

function unsolvePuzzle(p) {
  wait();
  if (p['oldstate'] != null) {
    p['state'] = p['oldstate'];
    p['solved'] = false;
    p['oldstate'] = null;
    drawPuzzle(p);
  }
  nowait();
}

var solve = function (p) {
  if (!p) return null;
  
  var result = false;
  
  // If we don't have solution, get it
  if (p['solution'] == null) {
    var data = jQuery.ajax({url: "/solve/?id="+p['puzzle_id'], 
	  type: "GET", 
	  async: false}).responseText;
    var json = eval ("("+data+")");
    p['solution'] = json['solution'];
  }
  p['oldstate'] = p['state'];
  p['state'] = p['solution'];
  p['solved'] = true;
  drawPuzzle(p);
};


var getBlockNumber = function (x, y, sqrt) {
  var p1 = Math.floor(x / sqrt);
  var p2 = Math.floor(y / sqrt);
  return p2 * sqrt + p1;
};


jQuery.sudoku = {
  'isCorrectSoFar': isCorrectSoFar,
  'compare': compare,
  'stringToMatrix': stringToMatrix,
  'getBlockNumber': getBlockNumber
};


jQuery.timestamp = function (date) {
  if (!date)
    date = new Date();
  var timestamp = 0;

  var seconds = date.getSeconds();
  var minutes = date.getMinutes();
  var hours = date.getHours();
  var day = date.getDate();
  var month = date.getMonth()+1;
  var year = date.getFullYear();
  // Days in month (non-leap year, leap year)
  var isLeapYear = (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0);
  var daysInMonth = [0, 31, (isLeapYear ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    
  var secondsInADay = 60 * 60 * 24;

  for (var i = 1970; i < year; i++) {
    var ly = (i % 4) == 0 && ((i % 100) != 0 || (i % 400) == 0);
    timestamp += secondsInADay * (ly ? 366 : 365);
  }
  for (var i = 1; i < month; i++)
    timestamp += secondsInADay * daysInMonth[i];
  timestamp += secondsInADay * day - 1;
  timestamp += (60 * 60 * hours) + (60 * minutes) + seconds;
  return timestamp;
};


$(function () {
    $('.puzzle').createPuzzle();
  });

