/*     automaton.js - Javascript cellular automaton
    Copyright (C) 2007 Aaron Altman

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

/* 
	The cells are represented by HTML tds.  They have a Javascript-
	attached member 'state' that tracks the automaton cell state.
	There are VIRTUAL_COLORS (constant) possible states.  These are
	estimated visually by COLOR_CLASSES (constant) CSS styles.
*/


/******************************************
	AUTOMATON INVARIANTS
*******************************************/

// Parameters to the Belousov-Zhabotinksy automaton algorithm
// With ~100 virtual colors, k1 = 2, k2 = 3 and g = 20 is a good
// shot at generating semi-periodic behavior.  g = 5 and 7 are cool
var HEIGHT = 34;
var WIDTH = 89;
var COLOR_CLASSES = 32;
var VIRTUAL_FACTOR = 3;
var VIRTUAL_COLORS = COLOR_CLASSES * VIRTUAL_FACTOR;
var K1 = 2;
var K2 = 2;
var G = 17;

// Constants enumerating positions of cells within a neighborhood
var UP_LEFT = 0;
var UP_MIDDLE = 1;
var UP_RIGHT = 2;
var LEFT = 3;
var MIDDLE = 4;
var RIGHT = 5;
var DOWN_LEFT = 6;
var DOWN_MIDDLE = 7;
var DOWN_RIGHT = 8;
var CELLS_IN_NEIGHBORHOOD = 9;

// A note about evolution functions:
// Function calls are very, very slow on Javascript.  They're poorly inlined,
// and have a low limit on recursive calls (something like 1000 in IE and Firefox,
// 3000 in Opera).  As such, I will inline the final revision code

// The most basic evolution function possible: don't change from the last frame
function trivialEvolve(cellNeighbor) {
	return cellNeighbor[MIDDLE];
}

// Evolution function for a 2D automaton using the Belousov-Zhabotinsky alg:
// Based on the values of the neighbors and current middle cell, return a
// new value for the middle cell.
function bzEvolveCells(cellNeighbor) {
	var illNeighbors = 0;
	var infectedNeighbors = 0;
	var i; // Loop variable
	var maxColor = VIRTUAL_COLORS - 1; // Optimization to limit out-of-scope variable lookup
	var returnValue = 0;

	for (i = 0; i < CELLS_IN_NEIGHBORHOOD; i++) {
		if (cellNeighbor[i] == maxColor) illNeighbors++;
		else if (cellNeighbor[i] > 0 && cellNeighbor[i] < maxColor) infectedNeighbors++;
	}

	// Ill cells become healthy.
	if (cellNeighbor[MIDDLE] == maxColor) {
		returnValue = 0;
	}

	// Healthy cells attain state floor(a/k1) + floor(b/k2)
	else if (cellNeighbor[MIDDLE] == 0) {
		returnValue = Math.floor(illNeighbors/K1) + Math.floor(infectedNeighbors/K2);
	}

	// Infected cells attain state s/(a + b + 1) + g where s is the sum of neighboring and self states
	else {
		returnValue = 0;

		for (i = 0; i < CELLS_IN_NEIGHBORHOOD; i++) returnValue += cellNeighbor[i];

		returnValue = returnValue / (K1 + K2 + 1) + G;
	}

	// Cells exceeding healthy state become healthy
	if (returnValue >= VIRTUAL_COLORS) {
		returnValue = maxColor;
	}

	return Math.round(returnValue);
} 

// end automaton invariants

/************************************
	AUTOMATON CLASS
 ************************************/

function constructAutomatonClass(domParent, outerContainerTag, thisTag, id, cssClass, rowTag, colTag, height, width, evolutionFunction) {
	var i;
	var j;

	// outerContainer will typically be a table.  
	// IE needs a tbody inside of it to apply styles
	// correctly.  
	var outerContainer = document.createElement(outerContainerTag);
	var automaton = document.createElement(thisTag);

	automaton.id = id;
	automaton.className = cssClass;
	outerContainer.id = id;
	automaton.evolve = evolutionFunction;

	// Arrays to track tr and td table members
	automaton.row = new Array(height);
	automaton.column = new Array(height * width); // Simulates a 2D array
	outerContainer.arrayHeight = height;
	outerContainer.arrayWidth = width;
	outerContainer.totalDimension = height * width;

	// May be used for animation
	outerContainer.finalFrame = new Array(height * width);

	// Set up table element arrays
	for (i = 0; i < height; i++) {
		automaton.row[i] = document.createElement(rowTag);

		// If the caller didn't pass in a previous frame, 
		// generate a random starting state.  Otherwise,
		// grab state from the previous frame.

		// OPTIMIZATION: the check for a starting frame
		// is invariant to the inner loop, so I pulled it
		// out.
		for (j = 0; j < width; j++) {
			var thisColumn = (width * i) + j;
			automaton.column[thisColumn] = document.createElement(colTag);

			automaton.column[thisColumn].state = Math.floor(Math.random() * VIRTUAL_COLORS);
			outerContainer.finalFrame[thisColumn] = automaton.column[thisColumn].state;
			automaton.column[thisColumn].className = 'state' + Math.floor(((automaton.column[thisColumn].state / VIRTUAL_FACTOR)));

			// Allow the user to draw on the automaton with the mouse 
			automaton.column[thisColumn].onmouseover = function() {
				this.state = 0;
				this.className = 'state0';
			}

			// Preserve HTML structure
			automaton.row[i].appendChild(automaton.column[thisColumn]);
		}

		automaton.appendChild(automaton.row[i]);
	}

	// This function really ought to copy all the cells into a new array.
	// For optimization reasons, we just return a direct reference and
	// trust the caller not to modify the contents.  If Javascript was
	// more formally object-oriented it could be done better.
	outerContainer.getCells = function() {
		return automaton.column;
	}

	outerContainer.iterate = function() {
		var totalDimension = this.totalDimension;
		var neighborhood = new Array(CELLS_IN_NEIGHBORHOOD);
		var column = this.automaton.column; // Save a few dereferences

		var upLeft = totalDimension - 1;
		var upMiddle = totalDimension - this.arrayWidth;
		var upRight = totalDimension - this.arrayWidth + 1;
		var left = this.arrayWidth - 1;
		var middle = 0;
		var right = 1;
		var downLeft = this.arrayWidth * 2 - 1;
		var downMiddle = this.arrayWidth;
		var downRight = this.arrayWidth + 1;

		for (middle = 0; middle < totalDimension; middle++) {
			neighborhood[0] = column[upLeft].state;
			neighborhood[1] = column[upMiddle].state;
			neighborhood[2] = column[upRight].state;
			neighborhood[3] = column[left].state;
			neighborhood[4] = column[middle].state;
			neighborhood[5] = column[right].state;
			neighborhood[6] = column[downLeft].state;
			neighborhood[7] = column[downMiddle].state;
			neighborhood[8] = column[downRight].state;

			// Evolve the current cell based on its neighbors
			// Original: this.finalFrame[middle] = this.automaton.evolve(neighborhood);

			var illNeighbors = 0;
			var infectedNeighbors = 0;
			var i; // Loop variable
			var maxColor = VIRTUAL_COLORS - 1; // Optimization to limit out-of-scope variable lookup
			var returnValue = 0;

			for (i = 0; i < CELLS_IN_NEIGHBORHOOD; i++) {
				if (neighborhood[i] == maxColor) illNeighbors++;
				else if (neighborhood[i] > 0 && neighborhood[i] < maxColor) infectedNeighbors++;
			}

			// Ill cells become healthy.
			if (neighborhood[MIDDLE] == maxColor) {
				returnValue = 0;
			}

			// Healthy cells attain state floor(a/k1) + floor(b/k2)
			else if (neighborhood[MIDDLE] == 0) {
				returnValue = Math.floor(illNeighbors/K1) + Math.floor(infectedNeighbors/K2);
			}

			// Infected cells attain state s/(a + b + 1) + g where s is the sum of neighboring and self states
			else {
				returnValue = 0;

				for (i = 0; i < CELLS_IN_NEIGHBORHOOD; i++) returnValue += neighborhood[i];

				returnValue = returnValue / (K1 + K2 + 1) + G;
			}

			// Cells exceeding healthy state become healthy
			if (returnValue >= VIRTUAL_COLORS) {
				returnValue = maxColor;
			}

			this.finalFrame[middle] = Math.round(returnValue);

			// Calculate the next positions together to save work
			upLeft = (upLeft + 1) % totalDimension;
			upMiddle = (upMiddle + 1) % totalDimension;
			upRight = (upRight + 1) % totalDimension;
			left = (left + 1) % totalDimension;
			right = (right + 1) % totalDimension;
			downLeft = (downLeft + 1) % totalDimension;
			downMiddle = (downMiddle + 1) % totalDimension;
			downRight = (downRight + 1) % totalDimension;
		}

		// Copy the final frame into the main.
		for (middle = 0; middle < totalDimension; middle++) {
			column[middle].state = this.finalFrame[middle];
			column[middle].className = 'state' + Math.floor((column[middle].state / VIRTUAL_FACTOR));
		}
	}

	outerContainer.appendChild(automaton);
	outerContainer.automaton = automaton;
	domParent.appendChild(outerContainer);

	// Send back the new automaton
	return outerContainer;
}

/**********************************
	ANIMATION INVARIANTS
 **********************************/

var STEPS = 20;
var TIME = 750;
var TIME_PER_FRAME = TIME / STEPS;
var OPACITY_PER_STEP = 10 / STEPS;
var DELAY = 250;

// end animation invariants

/***********************************
	PAGE SETUP
 ***********************************/

// When the page loads, set up the buffers and begin animating
window.onload = function() {
	var container = document.getElementById('automatonContainer');

	mainAutomaton = constructAutomatonClass(container, 'table', 'tbody', 'mainAutomaton', 'bufferTable', 'tr', 'td', HEIGHT, WIDTH, bzEvolveCells);
	
	mainAutomaton.animate = function() {
		this.iterate();

		// Recursive call after a timeout creates a time-based loop	
		setTimeout(this.id + '.animate();', TIME + DELAY);
	}

	mainAutomaton.animate();
}



