// Copyleft (k) 2008 by Jose Rodriguez (a.k.a. Boriel)
// Licensed under the GNU Public License 3.0 (GPL)
//
// Creates a Boriel Javascript Machine for Conway's Life Game
// Version: 1.0  (09/Ago/2008)
// Version: 1.1  (25/May/2010) --> Dropped MooTools in favour of JQuery

// Coordenate object
function BorielCoord(x, y)
{
	// Convert the coord 
	this.x = x;
	this.y = y;
	this.name = '_' + x + '_' + y;
}


function BorielObSub(ob)
{
	var r = [], i = 0;

	for (var z in ob)
		if (ob.hasOwnProperty(z))
			r[i++] = z;

	return r;
}



function BorielGetVarName(v)
{
	return BorielObSub(window).map(function(a) { 
             if (window[a] === v)
			 	return a;
			}).sort()[0];
}



// Boriel Javascript Machine
function BorielJM(id)
{
	var isFirefox = (/(Firefox)|(AppleWebKit)[\/\s](\d+\.\d+)/.test(navigator.userAgent));
	this.id = 'bm-' + id
	this.obj = document.getElementById(id);
	
	this.col = 0; // Column start
	this.row = 0; // Row start
	this.bgcolor = '#000000';
	this.fgcolor = '#A0A0A0';
	this.gridcolor = '#404040';

	// World dimensions
	this.wx = 25; // Columns
	this.wy = 25; // Rows
	this.changed = false; // True if world has changed
	this.timer_id = 0;
	this.time_slice = 500; // time slice in ms;
	this.running = false; // True if is running
	this.initial = false;
	
	this.cell_width = function() {
		return Math.floor(this.width / this.wx);
	};
		
	this.cell_height = function() {
		return Math.floor(this.height / this.wy);
	}
	
	if (isFirefox) { // ExCanvas IE is very slow
		this.wx *= 2;
		this.wy *= 2;
	}
	
	// Initializes machine internal states
	// Accoring to the parametes above (e.g. wx, wy, etc...)
	this.update = function() {
		var i, j, tmp, cv;

		// Get canvas surface dimensions
		tmp = this.obj.style.border;
		this.obj.style.border = 'none';

		this.width = $('#boriel-life').width()
		this.height = $('#boriel-life').height()
		//this.width = this.obj.getWidth(); // MooTools
		//this.height = this.obj.getHeight();

		this.obj.style.border = tmp;
		this.offx = this.obj.offsetLeft; 
		/* if (typeof(this.obj.clientLeft) != 'undefined')
			this.offx += this.obj.clientLeft; */
		this.offy = this.obj.offsetTop;
		/* if (typeof(this.obj.clientTop) != 'undefined')
			this.offy += this.obj.clientTop; */

//		cv = new Canvas({id: this.id + '-canvas', width: this.width, height: this.height });
		cv = document.createElement('canvas');

		if (typeof(this.canvas) == 'undefined') 
			this.obj.appendChild(cv);
		else
			this.obj.replaceChild(cv, this.canvas);

		cv.height = this.height;
		cv.width = this.width;
		cv.id = this.id + '-canvas';

		this.canvas = cv;
		this.canvas.boriel_vm = this;

		try {
			this.ctx = this.canvas.getContext('2d');
		} catch(e) {
		  return;
		}

		this.xx = this.cell_width();
		this.yy = this.cell_height();
		
		this.color = [this.bgcolor, this.fgcolor];
		//this.cell_imgs = [this.cellimg(0), this.cellimg(1)];
		
		this.world = new Array(this.wy);
		this.pending = {}; 
		
		for (i = 0; i < this.wy; i++) {
			this.world[i] = new Array(this.wx);
			for (j = 0; j < this.wx; j++) 
				this.world[i][j] = 0;
		}

		this.count = 0; // Generation counter
		/*this.obj.addEvent('click', this.onClick);*/
		$('#boriel-life').click(this.onClick);
		
		this.initial = false;

		// Paint the current status on the screen;
		this.paint();
	}
		
	// Redraws this surface completely
	this.paint = function() {
	    	var ctx = this.ctx;
		var xx = this.xx;
		var yy = this.yy;
		var x, y;

		if (this.initial) { // if already cached, restore
			ctx.restore();
			this.paint_world();
			return;
		}

		for (i = 0; i < this.wy; i++)
			for (j = 0; j < this.wx; j++) {
				var c = new BorielCoord(j, i);
				this.paint_cell(c, this.world[i][j]);
			}

		ctx.save();

		this.initial = true;
		this.paint_world();
	}

	this.cellimg = function(value) {
		var result, c;

		c = new BorielCoord(0, 0);
		this.ctx.fillStyle = this.color[value];
		this.ctx.fillRect(this.xx * c.x + 1, this.yy * c.y  + 1, this.xx - 1, this.yy - 1); 
		result = this.ctx.getImageData(1, 1, this.xx - 1, this.yy - 1);

		return result;
	}

			
	// Draws a cell using the given color.
	this.paint_cell = function(c, value) {
		//this.ctx.putImageData(this.cell_imgs[value], this.xx * c.x + 1, this.yy * c.y + 1);
		//return;
		var ctx = this.ctx;
		ctx.fillStyle = this.color[value];
		/*
		ctx.fillRect(this.xx * c.x + 1, this.yy * c.y  + 1, this.xx - 1, this.yy - 1); 
		return;
		*/
		var x1, y1, x2, y2; // This seems to be faster???

		x1 = this.xx * c.x + 1;
		y1 = this.yy * c.y + 1;
		x2 = x1 + this.xx - 1;
		y2 = y1 + this.yy - 1;
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x1, y2);
		ctx.closePath();
		ctx.fill();
	}
	
	
	// Returns if the given coord is pending or not
	this.is_pending = function(coord) {
		return typeof(this.pending[coord.name]) != 'undefined';
	}
	
	
	// North
	this.N = function(c) {
		return new BorielCoord(c.x, (c.y + this.wy - 1) % this.wy);
	}
	

	// South
	this.S = function(c) {
		return new BorielCoord(c.x, (c.y + this.wy + 1) % this.wy);
	}
	
	
	// East
	this.E = function(c) {
		return new BorielCoord((c.x + this.wx + 1) % this.wx, c.y);
	}
	
	
	// West
	this.W = function(c) {
		return new BorielCoord((c.x + this.wx - 1) % this.wx, c.y);
	}
	

	// North East
	this.NE = function(c) {
		return new BorielCoord((c.x + this.wx + 1) % this.wx, (c.y + this.wy - 1) % this.wy);
	}
	

	// North West
	this.NW = function(c) {
		return new BorielCoord((c.x + this.wx - 1) % this.wx, (c.y + this.wy - 1) % this.wy);
	}
	
	
	// South East
	this.SE = function(c) {
		return new BorielCoord((c.x + this.wx + 1) % this.wx, (c.y + this.wy + 1) % this.wy);
	}
	

	// South West
	this.SW = function(c) {
		return new BorielCoord((c.x + this.wx - 1) % this.wx, (c.y + this.wy + 1) % this.wy);
	}
		
	
	
	// Adds a point to pending list
	this.add_pending = function(c) {
		var q;

		q = c;
		this.pending[q.name] = q;
		q = this.N(c);
		this.pending[q.name] = q;
		q = this.S(c);
		this.pending[q.name] = q;
		q = this.E(c);
		this.pending[q.name] = q;
		q = this.W(c);
		this.pending[q.name] = q;

		q = this.NE(c);
		this.pending[q.name] = q;
		q = this.NW(c);
		this.pending[q.name] = q;
		q = this.SE(c);
		this.pending[q.name] = q;
		q = this.SW(c);
		this.pending[q.name] = q;
	}
	
	
	// Draws world
	this.paint_world = function() {
		var i, j, val, c;
		
		this.pending = {};
		
		for (i = 0; i < this.wy; i++)
			for (j = 0; j < this.wx; j++) {
				val = this.world[i][j];
				c = new BorielCoord(j, i);
				if (val) {
					this.paint_cell(c, val);
					this.add_pending(c);
				}
			}		
	}
	
	
	this.cell = function(c) {
		return this.world[c.y][c.x];
	}
	
	
	this.new_state = function(c) {
		//var sum = this.cell(this.N(c)) + this.cell(this.S(c)) + this.cell(this.E(c)) + this.cell(this.W(c)) +
		//	this.cell(this.NE(c)) + this.cell(this.NW(c)) + this.cell(this.SE(c)) + this.cell(this.SW(c));
		var x, y, wx, wy, i, j, sum = 0;

		wx = this.wx;
		wy = this.wy;

		for (y = c.y + wy - 1, i = -1; i < 2; y++, i++) {
			y %= wy;
			for (x = c.x + wx - 1, j = -1; j < 2; x++, j++) {
				if ((j == 0) && (i == 0)) continue;
				sum += this.world[y][x % wx];
			}
		}

		if (sum == 3)
			return 1;
			
		if (sum == 2)
			return this.world[c.y][c.x];
			
		return 0;
	}
	
	
	this.next = function() {
		var val, i, c, current, new_world;
			
		current = this.pending;
		new_world = {}; 
		this.pending = {};
		this.changed = false;

		for (i in current) {
			c = current[i];		
			val = this.new_state(c);
			if (val != this.cell(c)) { // Has changed?
				this.paint_cell(c, val);
				this.add_pending(c);
				c.val = val;
				new_world[c.name] = c;
				this.changed = true;
			}
		}
		
		for (i in new_world) {
			c = new_world[i];
			this.world[c.y][c.x] = c.val;
		}

		this.count++;
	}
	

	this.random = function() { // Fill with random points
		var i, j;
		
		for (i = 0; i < this.wy; i++)
			for (j = 0; j < this.wx; j++)
				this.world[i][j] = Math.floor(Math.random() * 2)
				
		this.initial = false;
		this.paint();
	}
	

	this.step = function() {
		if (this.running)
			return;

		this.running = true;

		if (this.changed) 
			this.next();
		else 
			this.stop();

		this.running = false;
	}
	
	this.run = function(callname) {
		if (this.timer_id) // Already running?
			return;
	
		this.changed = true;
		//this.timer_id = window.setInterval(BorielGetVarName(this) + '.step()', this.time_slice);
		this.timer_id = window.setInterval(callname, this.time_slice);
		this.call_interval = callname;
	}


	this.stop = function() {
		if (!this.timer_id)
			return;

		window.clearInterval(this.timer_id);
		this.timer_id = 0;
		this.running = false;
	}


	this.faster = function() {
		this.time_slice -= 50;

		if (this.time_slice < 50)
			this.time_slice = 50;

		this.stop();
		this.run(this.call_interval);
	}


	this.slower = function() {
		this.time_slice += 50;

		if (this.time_slice > 1000)
			this.time_slice = 1000;

		this.stop();
		this.run(this.call_interval);
	}


	this.onClick = function(ev) {
	//	var th = $(ev.target.id);
		var th = document.getElementById(ev.target.id);
		var col, row, c, val, err;

		try {
			th = th.boriel_vm;
		} catch (err) {
			alert('Error. Not supported in this browser.\nTry with Firefox 3.0');
			return false;
		}

		try {
			if (th.timer_id)
				return false;
		} catch (err) {
			// strange case where this might trigger an error in Firefox < 3.0
			// if clicked near the border
			alert(err);
			return false;
		}

	//	col = Math.floor((ev.page.x - th.offx) / th.xx);
	//	row = Math.floor((ev.page.y - th.offy) / th.yy);
		col = Math.floor((ev.pageX - this.offsetLeft) / th.xx);
		row = Math.floor((ev.pageY - this.offsetTop) / th.yy);

		if (col >= th.wx)
			col = th.wx - 1;

		if (row >= th.wy)
			row = th.wy - 1;

		val = th.world[row][col] = (1 + th.world[row][col]) & 1;
		c = new BorielCoord(col, row);
		th.paint_cell(c, val);
		th.add_pending(c);

		return false;
	}

	// Sets a configuration passed as an array.
	// The world is cleared and the given config is centered on the world.
	this.configure = function(arr) {
		var x, y, i, j;
		var timer = this.timer_id;
		
		this.stop();
		this.initial = false;
		for (i = 0; i < this.wy; i++)
			for (j = 0; j < this.wx; j++)
				this.world[i][j] = 0; // Clear the entire world

		y = Math.floor((this.wx - arr.length) / 2);
		x = Math.floor((this.wy - arr[0].length) / 2);

		for (i = 0; i < arr.length; i++)
			for (j = 0; j < arr[0].length; j++)
				this.world[y + i][x + j] = arr[i][j] % 2;

		this.paint();

		if (timer)
			this.run(this.call_interval);
	}


	// Returns an array with the current configuration
	this.get_config = function() {
		var i, j, w, h, x, X, y, Y, result;

		X = 0; Y = 0; x = this.wx - 1; y = this.wy - 1;
		for (i = 0; i < this.wy; i++) 
			for (j = 0; j < this.wx; j++) {
				if (!this.world[i][j]) 
					continue;

				if (X < j) X = j;
				if (x > j) x = j;
				if (Y < i) Y = i;
				if (y > i) y = i;
			}

		w = X - x + 1;
		h = Y - y + 1;
		result = '[';

		for (i = 0; i < h; i++) {
			result += '[';
			for(j = 0; j < w; j++)
				result += this.world[y + i][x + j] + ', ';
			result += '], ';
		}
		result += ']';

		return result;
	}


	this.update();
}


