/*
 * (c) 2008 David Harvey. All Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that
 * the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 
 *      following disclaimer.
 *   2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 
 *      the following disclaimer in the documentation and/or other materials provided with the distribution.
 *   3. The name of the author may not be used to endorse or promote products derived from this software without 
 *      specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * www.davethehat.com
 */

//==========================================
// Base grid object class (and direct implementation
// of null cell)
function GridObject(game, x, y) { this.init(game, x, y); }
GridObject.implement({
	init: function(game, x, y) {
		this.game = game;
		this.x = x;
		this.y = y;
	},

	getX: function() { return this.x; },
	getY: function() { return this.y; },
	
	reset        : function() { /* NOP */ },
	isAtom       : function() { return false; },
	isGuess      : function() { return false; },
	isPort       : function() { return false; },
	isCell       : function() { return false; },
	
	_fixUpMatrix : function() { /*NOP*/ },
	_nextCell: function(velocity) {
		var x = this.x + velocity[0][0];
		var y = this.y + velocity[1][0];
		return this.game.cellAt(x,y);
	},

});

//==========================================
// Cell - a single cell in the game
function Cell(game,x,y) { this.init(game,x,y);}

// Null cell - corners of board
Cell.nullCell = new GridObject();

// Matrices for ray transformations
Cell.MATRIX = {
	IDENTITY : [[ 1, 0],
				[ 0, 1]],
	ZERO     : [[ 0, 0],
				[ 0, 0]],
	REFLECT  : [[-1, 0],
				[ 0,-1]],
	DIAGR    : [[ 0, 1],
				[ 1, 0]],
	DIAGL    : [[ 0,-1],
				[-1, 0]],
	REFLECTH : [[-1, 0],
				[ 0, 1]],
	REFLECTV : [[ 1, 0],
				[ 0,-1]],
};

Cell.VELOCITY = {
	DOWN:	[[ 0],
			 [ 1]],	
	LEFT:	[[-1],
			 [ 0]],	
	UP:		[[ 0],
			 [-1]],	
	RIGHT:	[[ 1],
			 [ 0]],
	ZERO:	[[ 0],
			 [ 0]]
}

Cell.MatrixTimesVelocity = function(m, v) {
	ret = [[0],[0]];
	ret[0][0] = m[0][0] * v[0][0] + m[0][1] * v[1][0];
	ret[1][0] = m[1][0] * v[0][0] + m[1][1] * v[1][0];
	return ret;
}

Cell.InvertVelocity = function(v) {
	return Cell.MatrixTimesVelocity(Cell.MATRIX.REFLECT, v);
}

Cell.implement ( {
	init: function (game,x,y) {
		this.baseinit(game,x,y);
		this.guess  = false;
		this.atom   = false;
		this.matrix = Cell.MATRIX.IDENTITY;
	},
	
	reset: function() { this.guess = false; },

	isCell:  function() { return true; },
	isAtom:  function () { return this.atom; },
	setAtom: function (atom) {
		if (this.atom != atom) {
			this.atom = atom;
			this._fixMatrices();
			this.game.notify(this, BlackBox.events.ATOM_CHANGED);
		}
	},

	isGuess:  function() { return this.guess; },
	setGuess: function(guess) {
		if (this.guess != guess) {
			this.guess = guess;
			this.game.notify(this, BlackBox.events.GUESS_CHANGED);
		}
	},

	getExitV: function(entryV) {
		return Cell.MatrixTimesVelocity(this.matrix, entryV);
	},
		
	buildPath: function(path, entryV) {
		var exitV = this.getExitV(entryV);
		path.addElement(entryV, this, exitV);
		if (!exitV.deepEquals(Cell.VELOCITY.ZERO)) {
			var nextCell = this._nextCell(exitV);
			nextCell.buildPath(path, exitV);
		}
	},

	_fixMatrices: function() {
		this._getNeighbours().forEach(function (cell) {
			cell._fixUpMatrix();
		});
	},

	_getNeighbours: function() {
		if (!this.neighbours) {
			this.neighbours = this.game.getCellNeighbours(this.x, this.y); 
		}
		return this.neighbours;
	},
		
	_fixUpMatrix: function() {
		this.matrix = null;
		if (this.isAtom()) {
			this.matrix = Cell.MATRIX.ZERO;
		}
		else {
			var neighbours = this._getNeighbours();
			// Ensure that cells above/below have priority on edge
			if (this.getX() == 1 || this.getX() == this.game.getSize()) {
				var order = [3, 5, 1, 7];
			}
			else {
				var order = [1, 7, 3, 5];
			}
			// If atoms on adjacent orthogonal cells, then matrix
			// must reflect in two directions to allow for corner
			// cells. This cell can't otherwise be entered!
			var lastAtom = false;
			order.forEach(function(i){
				if (neighbours[i].isAtom()) {
					this.matrix = lastAtom ? Cell.MATRIX.REFLECT 
						: (i == 1 || i == 7) ? Cell.MATRIX.REFLECTH 
						: Cell.MATRIX.REFLECTV;
					lastAtom = true;
				}
				else {
					lastAtom = false;
				}
			},this);
			
			if (!this.matrix) {
				[0,2,6,8].forEach(function(i) {
					if (neighbours[i].isAtom()) {
						this.matrix = this.matrix ? Cell.MATRIX.IDENTITY 
								: (i % 4) ? Cell.MATRIX.DIAGR 
								: Cell.MATRIX.DIAGL;
					}
				},this);
			}
			
			if (!this.matrix) {
				this.matrix = Cell.MATRIX.IDENTITY;
			}
		}
	},
});

Cell.extend(GridObject);

//==========================================
// Path - starting and (possibly) ending at a Port, the
// path a ray takes through the grid of cells
function Path(trial) { return this.init(trial); }

Path.prototype = {
	init: function (trial) {
		this.trial = trial; 
		this.path = []; 
	},
	getLength:   function () { return this.path.length; },
	getTrial:    function () { return this.trial; },
	isAbsorbed:  function () { return this.endCell().isAtom(); },
	isReflected: function () { return this.startCell() === this.endCell(); },
	startCell:   function () { return this.path[0].cell; },
	endCell:     function () { return this.path[this.path.length - 1].cell; },
	addElement:  function (entryV, cell, exitV) {
		this.path.push(new PathElement(entryV, cell, exitV));
	},
	getElements: function () { return this.path; },
	destination: function(from) {
		return (this.startCell() === from ? this.endCell() : this.startCell());
	}
}

function PathElement(entryV, cell, exitV) {
	this.entryV = entryV;
	this.cell = cell;
	this.exitV = exitV;
}

//==========================================
// Port - entry/exit point for ray
function Port(game,x, y, exitV) {
	return this.init(game, x, y, exitV);
}

Port.implement( {
	init: function(game, x, y, exitV) {
		this.baseinit(game, x, y);
		this.exitV  = exitV;
		this.path   = null;
	},
	
	reset: function() { this.path = null; },
	
	isPort:   function() { return true; },
	getTrial: function() { return this.path.getTrial(); },

	getPath:        function() { return this.path; },
	getDestination: function() { return this.path && this.path.destination(this); },
	isAbsorbed:     function() { return this.path && this.path.isAbsorbed(); },	
	isReflected:    function() { return this.path && this.path.isReflected(); },
		
	isFired: function() { return this.path != null; },
	fire:    function() {
		if (!this.isFired()) {
			this.path = new Path(this.game.nextTry());
			this.buildPath(this.path);
			this.game.notify(this,BlackBox.events.PORT_FIRED);
			if (!this.isReflected() && !this.isAbsorbed()) {
				this.game.notify(this.getDestination(), BlackBox.events.PORT_FIRED);				
			}
		}
	},

	buildPath: function(path, entryV) {
		var nextCell;
		if (path.getLength() == 0) {
			this.path.addElement(Cell.VELOCITY.ZERO, this, this.exitV);
			nextCell = this._nextCell(this.exitV);
			nextCell.buildPath(this.path, this.exitV);
		} else {
			path.addElement(entryV, this, Cell.VELOCITY.ZERO);
			this.path = path;
		}
	},

} );

Port.extend(GridObject);

//==========================================
// BlackBox - the board, game objects and state
function BlackBox(size, atoms) {
	this.init(size, atoms);
}

BlackBox.constants = {
	DEFAULT_SIZE :  8,
	ATOM_SCORE   : 10,
	ATOM_PENALTY :  5 
}

BlackBox.events = {
	ATOM_CHANGED  : 'ATOM_CHANGED',
	GUESS_CHANGED : 'GUESS_CHANGED',
	PORT_FIRED    : 'PORT_FIRED',
}

BlackBox.implement( {
	init: function(size, atoms) {
		this.baseinit();
		this.tries    = 0;
		this.size     = size ? size : BlackBox.constants.DEFAULT_SIZE;
		this.numAtoms = atoms ? atoms : (this.size/2).integer();
		if (this.numAtoms == -1) {
			this.numAtoms = 0;
		}
		
		this.rows = new Array(this.size+2);
		for (var y = 0; y < this.size+2; y++) {
			
			var row = new Array(this.size+2);
			this.rows[y] = row;
			
			row[0] = row[this.size+1] = Cell.nullCell;
			for (var x = 1; x <= this.size; x++) {
				row[x] = (y == 0 || y == this.size+1) ? Cell.nullCell : new Cell(this, x, y);
			}
		}
				
		for (var i = 1; i <= this.size; i++ ) {
			this.rows[0][i]           = new Port(this,  i, 0,             Cell.VELOCITY.DOWN);
			this.rows[i][0]           = new Port(this,  0, i,             Cell.VELOCITY.RIGHT);
			this.rows[i][this.size+1] = new Port(this,  this.size + 1, i, Cell.VELOCITY.LEFT);
			this.rows[this.size+1][i] = new Port(this,  i, this.size + 1, Cell.VELOCITY.UP);
		}
		this._populateWithAtoms();
	},

	reset: function() {
		this.forEachCell(function(cell) { cell.reset();});
		this.tries = 0;
	},
		
	getSize:  function() { return this.size; },
	getTries: function() { return this.tries; },
	nextTry:  function() { return ++this.tries; },
	getScore: function() {
		var c = this.countCorrectGuesses();
		var u = this.numAtoms - c;
		var w = this.countGuesses() - c;
		var ret = {
			correct      : c,
			undiscovered : u,
			wrongGuesses : w,
			tries        : this.tries,
			score        :   c * BlackBox.constants.ATOM_SCORE
			               - u * BlackBox.constants.ATOM_PENALTY
			               - w * BlackBox.constants.ATOM_PENALTY
						   - this.tries
		};
		return ret;
	},
	
	cellAt: function (x,y) {
		if (x < 0 || y < 0 || x > this.size + 1 || y > this.size + 1) {
			return null;
		}
		return this.rows[y][x];
	},

	getCellNeighbours: function(x,y) {
		var ret = new Array(9);
		for (var i = 0; i < 9; i++) {
			var yo = (i/3).integer() - 1;
			var xo = i % 3 - 1;
			ret[i] = this.cellAt(x+xo,y+yo);
		}
		return ret;
	},

	forEachCell: function(f) {
		this.rows.forEach(function (row) {
			row.forEach(f,this);
		},this);
	},

	countCells: function(pred) {
		var count = 0;
		this.forEachCell(function (cell) {
			if (pred.call(this,cell)) {
				count++;
			}
		});
		return count;
	},

	countAtoms: function() {
		return this.countCells(function (cell) {return cell.isAtom();});
	},

	countGuesses: function() {
		return this.countCells(function (cell) {return cell.isGuess();});
	},

	countCorrectGuesses: function() {
		return this.countCells(function (cell) {return cell.isAtom() && cell.isGuess();});
	},

	_populateWithAtoms: function () {
		var limit = this.size * this.size;
		var nums  = new Array();
		while (nums.length < this.numAtoms) {
			var num = (Math.random() * limit).integer();
			if ((nums.filter(function (e) {return e == num;})).length == 0) {
				nums.push(num);
			}
		}
		nums.forEach(function(n) {
			var x = n % this.size + 1;
			var y = (n/this.size).integer() + 1;
			this.cellAt(x,y).setAtom(true);			
		},this);
	},
});

BlackBox.extend(Notifier);

