/**
 * A cell will either be [ .mine | .safe | <unassigned> ]
 * A cell that is .safe and .revealed will have [ .border-1 - .border-8 | .empty ]
 * The mine-bordering count is only calculated upon reveal, either by click or click-propagation
 * A cell that is .safe and NOT .revealed will not have any other state info
 * When the player dies, there will be a square that is .revealed.mine -- all other .revealed will always be .safe
 */
Minefield = function()
{
	Minefield = this;
	
	Minefield.$grid = $('#minefield');
	
	Minefield.newGame(18, 10, 20);
}

$.extend(Minefield.prototype,
{
	$grid:undefined,
	
	rows:undefined,
	cols:undefined,
	minesTotal:undefined,
	squaresTotal:undefined,
	
	minesUnassigned:undefined,
	squaresUnassigned:undefined,
	squaresStillHidden:undefined,
	
	isDead:undefined,
	
	/**
	 * Find the cell at the specified coordinates.
	 */
	getCell:function(rowDex, colDex)
	{
		if(rowDex < 0 || colDex < 0 || rowDex >= Minefield.rows || colDex >= Minefield.cols)
			return null;
			
		return Minefield.$grid.find('tr:eq('+rowDex+') > td:eq('+colDex+')').get(0);
	},
	
	/**
	 * Find the coordinates of the cell: [rowDex, colDex]
	 */
	getPos:function(cell)
	{
		var par = cell.parentNode;
		var rowDex = Minefield.$grid.find('tr').index(par);
		var colDex = $(par).find('td').index(cell);
		
		return [rowDex, colDex];
	},
	
	/**
	 * Return true if cell exists and is a mine, false otherwise.
	 * If necessary, calculate and assign status: .mine or .safe
	 */
	decideIsMine:function(cell)
	{
		if(cell === null)
			return false;
			
		var $cell = $(cell);
		
		if($cell.hasClass('mine'))
			return true;
		else if($cell.hasClass('safe'))
			return false;
			
		//first-click exemption
		if(Minefield.squaresUnassigned === Minefield.squaresTotal && Minefield.minesTotal < Minefield.squaresTotal)
		{
			$cell.addClass('safe');
			Minefield.squaresUnassigned--;
			return false;
		}
		
		//probability-based decision
		var willBe = ( Math.floor(Math.random() * Minefield.squaresUnassigned) < Minefield.minesUnassigned );
		
		if(willBe)
		{
			$cell.addClass('mine');
			Minefield.minesUnassigned--;
		}
		else
		{
			$cell.addClass('safe');
		}
		Minefield.squaresUnassigned--;
		return willBe;
	},
	
	/**
	 * Calculate, assign, and return the cell's mine-bordering count.
	 */
	discoverBorderCount:function(cell)
	{
		var basePos = Minefield.getPos(cell);
		var mCount = 0;
		//this iterates over the cell itself, but I don't care
		for(var rDex = -1; rDex <= 1; rDex++)
			for(var cDex = -1; cDex <= 1; cDex++)
				if(Minefield.decideIsMine(Minefield.getCell(basePos[0]+rDex, basePos[1]+cDex)))
					mCount++;
		$(cell).addClass( mCount === 0 ? 'empty' : 'border-'+mCount );
		return mCount;
	},
	
	/**
	 * Respond to a click on the board.
	 */
	clickField:function(e)
	{
		if(e.target.tagName.toLowerCase() !== 'td')
			return;
			
		var succeed = Minefield.tryClear(e.target);
		
		if(succeed && Minefield.squaresStillHidden === Minefield.minesTotal) // check for winning condition
			Minefield.win();
	},
	
	/**
	 * Try to clear the un-revealed cell.
	 * Return true if succeeded. (False if dead.)
	 */
	tryClear:function(cell)
	{
		if(cell === null)
			return true;
			
		//this test is here instead of in clickField because we want to be able to do quick'n'dirty recursion
		if($(cell).hasClass('revealed'))
			return true;
		
		// Cover both known and unassigned mines
		if(Minefield.decideIsMine(cell))
		{
			Minefield.die(cell);
			return false;
		}
		
		// Safe at this point, so set state
		var mCount = Minefield.discoverBorderCount(cell);
		$(cell).addClass('revealed');
		Minefield.squaresStillHidden--;
		
		//it is safe to recurse since state has already been set in discoverBorderCount
		if(mCount === 0)
		{
			setTimeout(function(){
				var basePos = Minefield.getPos(cell);
				for(var rDex = -1; rDex <= 1; rDex++)
					for(var cDex = -1; cDex <= 1; cDex++)
						Minefield.tryClear(Minefield.getCell(basePos[0]+rDex, basePos[1]+cDex));
			}, 10);
		}
		
		return true;
	},
	
	/**
	 * Assign and reveal rest of mines.
	 */
	die:function(cell)
	{
		Minefield.$grid.unbind('click');
		Minefield.$grid.addClass('lost');
		$(cell).addClass('revealed');
		Minefield.$grid.find('td:not(.safe):not(.mine)').each(function()
		{
			if(Minefield.minesUnassigned > 0)
				Minefield.decideIsMine(this);
			else
				return false;
		});
	},
	
	/**
	 * Assign and reveal rest of mines.
	 */
	win:function()
	{
		Minefield.$grid.unbind('click');
		Minefield.$grid.addClass('won');

		Minefield.$grid.find('td.mine').each(function()
		{
			$(this).addClass('flag');
		});
	},
	
	/**
	 * 
	 */
	newGame:function(rows, cols, mines)
	{
		if(rows < 1 || cols < 1)
			throw new Exception("Cannot have width or height less than 1: (rows="+rows+", cols="+cols+")");
		Minefield.rows = rows;
		Minefield.cols = cols;
		
		Minefield.$grid.empty().removeClass('lost').removeClass('won');
		
		var firstRow = Minefield.$grid.get(0).insertRow(-1);
		
		for(var cellDex = 0; cellDex < cols; cellDex++)
		{
			firstRow.insertCell(-1);
		}
		
		var lastRow = firstRow;
		var curRow = undefined;
		for(var rowDex = 1; rowDex < rows; rowDex++)
		{
			curRow = lastRow.cloneNode(true);
			lastRow.parentNode.appendChild(curRow);
			lastRow = curRow;
		}
		
		Minefield.minesTotal = mines;
		Minefield.minesUnassigned = mines;
		Minefield.squaresTotal = rows * cols;
		Minefield.squaresUnassigned = Minefield.squaresTotal;
		Minefield.squaresStillHidden = Minefield.squaresTotal;
		
		Minefield.$grid.click(Minefield.clickField);
	}
});

$(document).ready(function()
{
	new Minefield();
});

