// Grid.js --- a cellular automaton implementing Conway's Game of Life
// in a toroidal universe.
//
// Copyright (C) Emmet Caulfield, 2003. All rights reserved.
//
// You may use this software freely provided that you do not remove,
// alter, obscure, or otherwise conceal my authorship of it. In
// particular, removing or hiding the link to netrogen.com in the
// bottom-right cell is expressly prohibited.
// 
// This implementation is very slow and not at all
// optimised. The limiting factor is going to be the
// big, slow DOM, not the code, so there's no point
// optimising. This is just for fun anyway.

// Cell constructor. The cell knows its position in
// the grid for debugging.
function Cell(obj)
{
    this.state=false;
    this.lastState=false;
    this.neighborhood;
    this.domNode=obj;
}


Cell.prototype.toggle = function()
{
    this.state=!this.state;
}

// Using Grid.RandomColor() here is a *severe* violation of encapsulation,
// but, hey, it's just a little JavaScript!
Cell.prototype.update = function(offColor,onColor)
{
    if( this.state != this.lastState ) {
	this.domNode.style.backgroundColor = this.state ? onColor : Grid.RandomColor();
	this.lastState=this.state;
	return true;
    }
    return false;
}

// Grid constructor
function Grid(rows,cols,size)
{
    var i;

    this.maxRows=rows;
    this.maxCols=cols;
    this.size=size;
    this.loopTimer=null;
    this.stepTime=null;
    this.id = Grid.LIST.length;
    Grid.LIST[this.id]=this;
    this.buf=new Array(rows);
    for(i=0; i<rows; i++) {
	this.buf[i]=new Array(cols);
    }
    this.onColor  = '#000000'; // Default color for 'off' cell is white
    this.offColor = '#ffffff'; // Default color for 'on' cell is black
    this.operation = null;
    this.opName = null;
    this.varName = null;
    this.generation = 0;
    this.lastGeneration = 0;
    this.showStatus = false;
}

// A list of all of the available grids:
Grid.LIST = new Array();

// The Grid palette used to make death a little less dreary
Grid.Palette = new Array(16);
Grid.Palette[0]  = '#dfefff';
Grid.Palette[1]  = '#dfffef';
Grid.Palette[2]  = '#efdfff';
Grid.Palette[3]  = '#efffdf';
Grid.Palette[4]  = '#ffdfef';
Grid.Palette[5]  = '#ffefdf';
Grid.Palette[6]  = '#dfefef';
Grid.Palette[7]  = '#dfffff';
Grid.Palette[8]  = '#efdfdf';
Grid.Palette[9]  = '#efffff';
Grid.Palette[10] = '#ffdfdf';
Grid.Palette[11] = '#ffefef';
Grid.Palette[12] = '#dfdfef';
Grid.Palette[13] = '#dfdfff';
Grid.Palette[14] = '#efefff';
Grid.Palette[15] = '#ffffdf';

// Universal URIs for helper files (none yet)
Grid.URI = new Object;
Grid.URI.Base = '';
Grid.URI.help = 'cgol/help.html';

Grid.URI.copyright = 'http://www.netrogen.com/';

Grid.URI.load = 'cgol/load.html';


// To route setTimer() timers back to the
// correct Grid object in the LIST:
Grid.RouteTimer = function(id,fn) 
{
    obj=Grid.LIST[id];
    eval("obj."+fn);
}

// To get mouse-clicks back to the right Grid object:
Grid.RouteClick = function(id, i, j)
{
    Grid.LIST[id].toggle(i,j);
}


// A generate a "random" color:
Grid.RandomColor = function()
{
    var c=Grid.Palette[ Math.floor(16*Math.random()) ];
    return c;
}

// Update a the on-screen Grid from the buffer
Grid.prototype.update = function()
{
    var i,j; // Row, col iterators
    var unchanged = 0;

    for(i=0; i<this.maxRows; i++) {
	for(j=0; j<this.maxCols; j++) {
	    if( ! this.buf[i][j].update(this.offColor, this.onColor) ) {
		unchanged++;
	    }
	}
    }
    if( unchanged == (this.maxRows * this.maxCols) ) {
	return false;
    } 
    if( this.showStatus ) {
	window.status = this.opName +': Generation ' +this.generation +', '+ unchanged + ' of '+ this.maxRows * this.maxCols + ' cells unchanged';
    }
    return true;
}

// Emit the HTML for a Grid:
Grid.prototype.writeHTMLTable = function()
{
    var i,j;
    if( this.showStatus ) {
	window.status = 'Writing Cellular Automaton';
    }
    if( !document.getElementById ) {
	document.writeln('<img src="/img/cgol.png" width="582" height="292" alt="Your browser is not DOM compliant. This image is shown instead of a DHTML/JavaScript cellular automaton" />');
	return false;
    }

    document.writeln('<table class="grid">');
    for(i=0; i<this.maxRows; i++) {
	//	document.write('<tr style="height:'+this.size+'px">');
	document.write('<tr>');
	for(j=0; j<this.maxCols; j++) {
	    cid=this.domId(i,j);
	    if( i==this.maxRows-1 && j==this.maxCols-1) {
		document.write('<td id="'+cid+'" name="'+cid+'"><span style="font-family: monospace; font-size:8pt; color:blue; cursor:pointer" onclick="Grid.Copyright()">i</a></td>'); 
	    } else {
		document.write('<td id="'+cid+'" name="'+cid+'"></td>'); 
	    }
	}
	document.writeln('</tr>');
    }
    document.writeln('</table>');
    this.initBuffer();
    return true;
}

// Emit the controls for a grid. MUST be called with the name of the
// variable that the controls are expected to control. Not the best
// way of solving this problem, but the most expedient. A better
// solution would be to register 'this'-specific callbacks via
// scripting.
Grid.prototype.writeHTMLControls = function(v)
{
    this.varName = v;
    document.writeln('<p class="gridControl">');
    document.writeln('<a href="javascript:'+v+'.load()">Load</a> |');
    document.writeln('<a href="javascript:'+v+'.save()">Save</a> |');
    document.writeln('<a href="javascript:'+v+'.faster()">Faster</a> |');
    document.writeln('<a href="javascript:'+v+'.slower()">Slower</a> |');
    document.writeln('<a href="javascript:'+v+'.reset()">Clear</a> |');
    document.writeln('<a href="javascript:'+v+'.stop()">Stop</a> |');
    document.writeln('<a href="javascript:'+v+'.go()">Go</a> |');
    document.writeln('<a href="javascript:'+v+'.help()">?</a>');
    document.writeln('</p>');
}

// Clear the automaton and reset the generation count:
Grid.prototype.reset = function()
{
    var i,j;
    if( this.showStatus ) {
	window.status = this.opName = ': Reset';
    }

    this.stop();
    for(i=0; i<this.maxRows; i++) {
	for(j=0; j<this.maxCols; j++) {
	    this.buf[i][j].state=false;
	}
    }
    this.generation=0;
    this.update();
}

// Pretty redundant really, since we're letting clients set internal
// stuff directly.
Grid.prototype.setStepTime = function(ms)
{
    this.stepTime=ms;
    if( this.showStatus ) {
	window.status = this.opName = ': Step-time set to ' + ms + 'ms';
    }
}

// Directly accesses the Universe and colors it in. A vestige of the
// development process.
Grid.prototype.randomColors = function()
{
    clearInterval(this.loopTimer);
    this.randomCellNode().style.backgroundColor=Grid.RandomColor();
    this.operation = 'randomColors()';
    this.opName = 'Random Colours';
    this.next();
}

// Toggles the state of random a random cell in each generation. A
// vestige of the development process.
Grid.prototype.reversi = function()
{
    clearTimeout(this.loopTimer);
    c=this.randomCell();
    c.toggle();
    //    window.status=c.row+','+c.col+':'+c.state+','+c.lastState;;
    if( !this.generation ) {
	this.operation = 'reversi()';
	this.opName    = 'Reversi';
    }
    this.generation++;
    this.update();
    this.next();
}

// cgol() implements the rules of John Conway's "Game of
// Life". Counting of the live cells in the Moore neighbourhood of
// each cell is done by mooreUpdate(), below.
// 
// See: // http://www.ibiblio.org/lifepatterns/october1970.html
Grid.prototype.cgol = function()
{
    var i,j; // Row, column iterators
    var c;   // Cell reference

    clearTimeout(this.loopTimer);
    this.mooreUpdate();
    for(i=0; i<this.maxRows; i++) {
	for(j=0; j<this.maxCols; j++) {
	    c=this.buf[i][j];
	    if (c.neighborhood == 3) {
		c.state=true;
	    } else if (c.state && c.neighborhood == 2) {
		c.state=true;
	    } else {
		c.state=false;
	    }
	}
    }
    if( !this.generation ) {
	this.operation = 'cgol()';
	this.opName    = "Conway's Game of Life";
    }
    this.generation++;
    if( this.update() ) {
	this.next();
    } else {
	this.stop();
    }
}

Grid.prototype.next = function()
{
    if(this.lastGeneration && this.lastGeneration <= this.generation ) {
	this.stop();
	return false;
    }
    this.loopTimer=setTimeout("Grid.RouteTimer("+this.id+", '" +this.operation+ "')", this.stepTime);
    return true;
}

Grid.prototype.start = function() {
    this.next();
}

Grid.prototype.go = function() {
    if( this.operation == null ) {
	this.operation = 'cgol()';
    }
    this.lastGeneration = 0;
    this.next();
}

Grid.prototype.getNodeFromDom = function(i,j) {
    return document.getElementById(this.domId(i,j));
}

Grid.prototype.domId = function(i,j) {
    return 'g'+this.id+'_'+i+'_'+j;
}

Grid.prototype.initBuffer = function()
{
    var i,j;
    var td;
    if( this.showStatus ) {
	window.status = 'Initialising off-screen buffer';
    }
    for(i=0; i<this.maxRows; i++) {
	for(j=0; j<this.maxCols; j++) {
	    td = this.getNodeFromDom(i,j);
	    td.gridId = this.id;
	    td.gridRow = i;
	    td.gridCol = j;
	    // Note that 'this' here refers to the table cell in the
	    // DOM, not 'this' Grid object:
	    td.onclick=function(){ Grid.RouteClick(this.gridId, this.gridRow, this.gridCol) };
	    this.buf[i][j]=new Cell( td );
	}
    }
}

// Calculates the number of live cells in the Moore
// neighborhood of cell at index [row][col] in a
// toroidal universe.
Grid.prototype.mooreSum = function(row,col) 
{
    var i=0;      // Raw row index 
    var j=0;      // Raw column index
    var m=0;      // Row index in toroidal universe
    var n=0;      // Column index in toroidal universe
    var total=0;  // Live cell total in Moore neighbourhood

    //    window.status=row+','+col;
    for(i=row-1; i<=row+1; i++) {
	m = (i < 0 ? (this.maxRows-1) : (i%this.maxRows));
	for(j=col-1; j<=col+1; j++) {
	    n = (j < 0 ? (this.maxCols-1) : (j%this.maxCols));
	    c = this.buf[m][n];
	    if( !(i==row && j==col) && c.state) {
		total++;
	    }
	}
    }
    return total;
}

Grid.prototype.mooreUpdate = function()
{
    var i=0;
    var j=0;
    var c=null;

    if( this.showStatus ) {
	window.status = this.opName + ': Computing totals in Moore neighbourhoods';
    }
    for(i=0; i<this.maxRows; i++) {
	for(j=0; j<this.maxCols; j++) {
	    c=this.buf[i][j];
	    c.neighborhood=this.mooreSum(i,j);
	}
    }
}


Grid.prototype.setCell = function(i,j)
{
    if( i<0 || j<0 || i >= this.maxRows || j >= this.maxCols ) {
	return false;
    }
    this.buf[i][j].state=true;
    return true;
}

Grid.prototype.clearCell = function(i,j)
{
    if(i<0 || j<0 || i >= this.maxRows || j >= this.maxCols) {
	return false;
    }
    this.buf[i][j].state=false;
    return true;
}

Grid.prototype.toggle = function(i,j) {
    //    alert(i+','+j);
    var c=this.buf[i][j];
    c.toggle();
    c.update(this.offColor, this.onColor);
}


Grid.prototype.stop=function()
{
    clearTimeout(this.loopTimer);
    if( this.showStatus ) {
	window.status = this.opName + ': Stopped at generation ' + this.generation;
    } else {
	window.status = '';
    }
}


Grid.prototype.save=function()
{
    var i,j;
    var w=window.open('', 'GridDump', 'width=500,height=500,scrollbars,resizable');
    var d=w.document.open('text/html');
    var l;
    d.write('<pre>');
    for(i=0; i<this.maxRows; i++) {
	l='';
	for(j=0; j<this.maxCols; j++) {
	    l += this.buf[i][j].state ? 'O' : '.';
	}
	d.writeln(l);
    }
    d.write('</pre>');
    d.close();
}


Grid.prototype.load = function()
{
    var w=window.open('', 'GridLoad', 'width=400,height=400,scrollbars,resizable');
    var d=w.document.open('text/html');
    d.writeln('<html><head><title>Load Patterns</title></head>');
    d.writeln('<body><p>Paste pattern here:</p>');
    d.writeln('<form action=""><p>');
    d.writeln('<textarea style="font-family:monospace" id="pattern" name="pattern" rows="' +(this.maxRows+2)+ '" cols="' +(this.maxCols+10)+ '"></textarea>');
    d.writeln('<br />');
    d.writeln('<input type="button" value="Load this Pattern" onclick="opener.'+this.varName+'.loadPattern(pattern.value)" />');
    d.writeln('<input type="button" value="Clear" onclick="pattern.value=\'\'" />');
    d.writeln('<br />');
    d.writeln('Cut &amp paste patterns from: <a href="http://www.argentum.freeserve.co.uk/lex.htm" target="_new">Game of Life Lexicon</a>');
    d.writeln('</p></form></body></html>');
    d.close();
}    


// A 'pattern' from the Lexicon of Life consists of 'O's (capital
// letter 'o'), representing live cells, and '.'s, representing
// dead cells. Lines in the pattern text are rows in the 
// automaton's grid.
//
// If 'keep' is null or false, the universe is cleared.
//
// 'row' and 'col' specify the position where the upper-left corner of
// the pattern is to be placed. If 'row' is not supplied or otherwise
// null, the pattern is centered amongst the available rows in the
// Universe; 'col' is handled analogously.
Grid.prototype.loadPattern = function(pat,keep,row,col)
{
    var i,j,k;
    var offsetRow, offsetCol;

    var line=pat.split("\n");
    var numRows;
    var numCols=0;

    // Remove any character that is not a 'O' (capital letter 'o') or
    // a '.' from each line:
    for(i=0; i<line.length; i++) {
	var l=line[i].length;
	for(j=0; j<line[i].length; j++) {
	    while( j<line[i].length && !(line[i].charAt(j) == 'O' || line[i].charAt(j)=='.') ) {
		line[i] = line[i].substring(0,j) + line[i].substring(j+1,line[i].length);
	    }
	}
    }

    // Delete empty leading lines:
    while( 0 == line[0].length ) {
	for(i=0; i<line.length-1; i++) {
	    line[i]=line[i+1];
	}
	line.length=line.length-1;
    }

    // Delete empty trailing lines:
    while( 0 == line[line.length-1].length ) {
	line.length = line.length-1;
    }

    numRows=line.length;
    for(i=0; i<numRows; i++) {
	if( numCols < line[i].length ) {
	    numCols = line[i].length;
	}
    }

    if( numRows > this.maxRows ) {
	alert('Too many rows in pattern');
	return;
    }
    if( numCols > this.maxCols ) {
	alert('Too many columns in pattern');
	return;
    }
    if( keep == null || keep == false ) {
	this.reset();
    }
    
    offsetRow = row==null ? Math.round(this.maxRows/2 - numRows/2) : row;
    offsetCol = col==null ? Math.round(this.maxCols/2 - numCols/2) : col;
    //    alert(numRows+','+numCols+':'+offsetRow+','+offsetCol)

    for(i=0; i<numRows; i++) {
	for(j=0; j<numCols; j++) {
	    if( j<line[i].length && line[i].charAt(j) == 'O' ) {
		this.setCell(offsetRow+i,offsetCol+j);
	    }
	}
    }
    this.update();
	    
}


Grid.prototype.stopAtGeneration = function(g) {
    this.lastGeneration = g;
}

Grid.prototype.faster=function(m)
{
    var t;
    if(!m) {
	m=0.9;
    }
    this.stepTime = Math.round(m*this.stepTime);
}


Grid.prototype.slower=function(m)
{
    if(!m) {
	m=1.1;
    }
    this.stepTime = Math.round(m*this.stepTime);
}


Grid.prototype.setBlock = function(row,col,size)
{
    var i,j;
    for(i=row; i<=row+size; i++) {
    	for(j=col; j<=col+size; j++) {
    	    this.setCell(i,j);
    	}
    }
}

Grid.prototype.clearBlock = function(row,col,size)
{
    var i,j;
    for(i=row; i<=row+size; i++) {
    	for(j=col-1; j<=col+size; j++) {
    	    this.clearCell(i,j);
    	}
    }
}


Grid.prototype.bign=function(row,col,s)
{
    var i,j;

    for(i=0; i<=s; i++) {
	this.setCell(row+i,col);      // Left stroke left
	this.setCell(row+i,col+1);    // Left stroke right
	this.setCell(row+i,col+s);    // Right stroke left
	this.setCell(row+i,col+s+1);  // Right stroke right
	this.setCell(row+i,col+i);    // Cross stroke bottom
	this.setCell(row+i-1,col+i);  // Cross stroke top
    }
    this.clearCell(row-1,col);        // Extra pixel from cross-stroke 
    this.setCell(row,col-1);          // Top left serif
    this.setCell(row+s,col-1);        // Bottom right serif left
    this.setCell(row+s,col+2);        // Bottom left serif right
    this.setCell(row+s,col+s+2);      // Bottom right serif 
    this.setCell(row,col+s-1);        // Top right serif left
    this.setCell(row,col+s+2);        // Top right serif right
}


// Use the loadPattern() functionality, to load any pattern. We use an
// Object as an associative array.
Grid.BitMap=new Object;

Grid.BitMap['blinker']="OOO";

Grid.BitMap['block']="OO\nOO";

Grid.BitMap['glider']=
    'OO.'  +"\n"+
    'O.O'  +"\n"+
    'O..'  +"\n";

Grid.BitMap['rpentomino']=
    '.OO'  +"\n"+
    'OO.'  +"\n"+
    '.O.'  +"\n";

Grid.BitMap['H7']=
    '0.....O'  +"\n"+
    'O000000'  +"\n"+
    'O.....O'  +"\n";

Grid.BitMap['hertz']=
    '...OO.O....'  +"\n"+  
    '...O.OO....'  +"\n"+
    '...........'  +"\n"+
    '....OOO....'  +"\n"+
    '...O...O.OO'  +"\n"+
    '...O.O.O.OO'  +"\n"+
    'OO.O...O...'  +"\n"+
    'OO.O...O...'  +"\n"+
    '....OOO....'  +"\n"+
    '...........'  +"\n"+
    '....OO.O...'  +"\n"+
    '....O.OO...'  +"\n";

Grid.BitMap['hwss']=
    '..OO...'  +"\n"+
    'O....O.'  +"\n"+
    '......O'  +"\n"+
    'O.....O'  +"\n"+
    '.OOOOOO'  +"\n";

Grid.BitMap['koksGalaxy']=
    'OOOOOO.OO'  +"\n"+
    'OOOOOO.OO'  +"\n"+
    '.......OO'  +"\n"+
    'OO.....OO'  +"\n"+
    'OO.....OO'  +"\n"+
    'OO.....OO'  +"\n"+
    'OO.......'  +"\n"+
    'OO.OOOOOO'  +"\n"+
    'OO.OOOOOO'  +"\n";


// Similar calling convention to loadPattern()
Grid.prototype.loadNamedPattern = function(name,keep,i,j) {
    this.loadPattern(Grid.BitMap[name],true,i,j);
}

// Alteration of this function is prohibited.
Grid.Copyright = function()
{
    alert('Netrogen\'s interactive JavaScript/DHTML implementation of'  +"\n\n"+
	  '             John Conway\'s "Game of Life"                '  +"\n\n"+
	  'in a toroidal universe'					+"\n\n"+
	  'http://www.netrogen.com/'
	  ); 
}


Grid.prototype.help = function()
{
    window.open(Grid.URI.help, 'GridHelp', 'width=500,height=600,scrollbars,resizable');
}


// Not used by CGOL
Grid.prototype.randomColumnIndex = function()
{
    return Math.floor(this.maxCols*Math.random());
}

Grid.prototype.randomRowIndex = function()
{
    return Math.floor(this.maxRows*Math.random());
}

Grid.prototype.randomCellNode = function()
{
    return this.buf[this.randomRowIndex()][this.randomColumnIndex()].domNode;
}

Grid.prototype.randomCell = function()
{
    return this.buf[this.randomRowIndex()][this.randomColumnIndex()];
}
