/*
 "Gra w życie"
 Autor: Adam "Adawo" Wójs
 Licencja: GPLv3
 */

function GameOfLife() {
	var map = []; 		/* Tablica reprezentująca stan wszytskich komórek na obszarze gry */
	var initMap = []; 	/* Układ początkowy, na potrzeby this.backToInit () */
	var playground; 	/* Element DOM w którym ma znajowac się obszar gry */
	var stats = {
		step : 0,		/* Krok ewolucji */
		aliveCells : 0	/* Ilość żywych komórek w aktualnym kroku ewolucji */
	}; 	
	
	/* Domyślne opcje gry */
	var options = {
		rows: 30, 		/* Ilość wierszy */
		cols: 30, 		/* Ilość kolumn */
		interval: 500, 	/* Szybkość ewolucji */
		onStep: null, 	/* Funkcja wykonywana co krok, argumentem jest wartość kroku*/
		onStatsUpdate : null 
	}
	
	/* Metody pomocnicze: */
	/* Zwraca element DOM reprezentujący komórke o współrzędnych @x, @y */
	var getCell = function(x, y) {
		return $("tr", playground).eq(x).children().eq(y);
	}
	
	/* "Rysuje" martwą komórke o współrzędnych @x, @y */
	var drawDeadCell = function(x, y) {
		getCell(x, y).removeClass("life");
	}
	
	/* "Rysuje" żywą komórke o współrzędnych @x, @y */
	var drawLiveCell = function(x, y) {
		getCell(x, y).addClass("life");
	}
	
	/* Zmienia stan komórki o współrzędnych @x, @y na @alive (true - żywa, false - nieżywa) */
	var setCellState = function(x, y, alive) {
		map[x][y] = alive;
		if (alive) 
			drawLiveCell(x, y);
		else 
			drawDeadCell(x, y);
	}
	
	/* Wywołuje funkcje @f na kazdej komórce, przekazuja jako parametr współrzędne x, y komórki */
	var eachCell = function(f) {
		for (var x = 0; x != options.rows; ++x) 
			for (var y = 0; y != options.cols; ++y) 
				if (f(x, y)) break;
	}
	
	/* Czy komórka o współrzędnych @x, @y jeszcze żyje ? */
	var stillAlive = function(x, y) {
		return map[x][y];
	}
	
	/* Zwraca liczbę żywych sąsiadów komórki o współrzędnych @x, @y, wg. moora */
	var livingNeighbors = function(x, y) {
		var livingCells = 0;
		
		/* NW sąsiad */
		if (x > 0 && y > 0 && stillAlive(x - 1, y - 1)) 
			++livingCells;
		/* N sąsiad */
		if (y > 0 && stillAlive(x, y - 1)) 
			++livingCells;
		/* NE sąsiad */
		if (x < options.cols - 1 && y > 0 && stillAlive(x + 1, y - 1)) 
			++livingCells;
		/* E sąsiad */
		if (x < options.cols - 1 && stillAlive(x + 1, y)) 
			++livingCells;
		/* SE sąsiad */
		if (x < options.cols - 1 && y < options.rows && stillAlive(x + 1, y + 1)) 
			++livingCells;
		/* S */
		if (y < options.rows && stillAlive(x, y + 1)) 
			++livingCells;
		/* SW */
		if (x > 0 && y < options.rows && stillAlive(x - 1, y + 1)) 
			++livingCells;
		/* W */
		if (x > 0 && stillAlive(x - 1, y)) 
			++livingCells;
		
		return livingCells;
	}
	
	/* Wyszukuje krawędzie wzoru */
	var getEdges = function(map) {
		var edges = {
			xmin: options.rows,
			ymin: options.cols,
			xmax: 0,
			ymax: 0
		}
		
		for (var x = 0; x != options.rows; ++x) 
			for (var y = 0; y != options.cols; ++y) 
				if (map[x][y]) 
				{
					if (x < edges.xmin) edges.xmin = x;
					if (x > edges.xmax) edges.xmax = x;
					
					if (y < edges.ymin) edges.ymin = y;
					if (y > edges.ymax) edges.ymax = y;
				}
		
		return edges;
	}	
	
	/* Uaktualnia statystyki żywych komórek */
	stats.update = function () {
		stats.aliveCells = 0;
		eachCell (function (x, y) {
			if (map[x][y])
				++stats.aliveCells;
		})
		if (options.onStatsUpdate)
			options.onStatsUpdate ();
	}
	
	/* Inicjalizuje gre */
	this.init = function(o, newOptions) {
		playground = o;
		$.extend(options, newOptions);
		
		var table = $("<table>").appendTo(o).attr("cellspacing", 1).addClass("ui-state-default ui-corner-all");
		
		map = new Array(options.rows);
		for (var x = 0; x != options.rows; ++x) 
		{
			map[x] = new Array(options.cols);
			var row = $("<tr>").appendTo(table);
			
			for (var y = 0; y != options.cols; ++y) 
			{
				$("<td>").appendTo(row).addClass("ui-widget");
				map[x][y] = false;
			}
		}		
		initMap = map;
		
		/* Funkcja pozwalająca na inicjalizacje komórek (tylko w zerowym kroku) */
		$("td").click(function() {
			if (stats.step == 0) 
			{
				$(this).toggleClass("life", !$(this).hasClass("life"));
				
				var cellIndex = $(this).get(0).cellIndex;
				var rowIndex = $(this).parent().get(0).rowIndex;
				map[rowIndex][cellIndex] = $(this).hasClass("life");
			
				if (map[rowIndex][cellIndex]) 
					++stats.aliveCells; 
				else
					--stats.aliveCells;
				if (options.onStatsUpdate)
					options.onStatsUpdate ();	
			}
		});
	}
	
	/* Wykonuje krok w ewolucji komórek*/
	this.doStep = function() {
		/* Tworzymy kopie w initMap, pierwszej generacji... */
		if (stats.step == 0) 
			initMap = map;
		
		stats.aliveCells = 0;
		var newMap = new Array(options.rows);
		for (var x = 0; x != options.rows; ++x) 
		{
			newMap[x] = new Array(options.cols);
			for (var y = 0; y != options.cols; y++) 
			{
				var neighbors = livingNeighbors(x, y);
				if (!stillAlive(x, y)) 
				{
					if (neighbors == 3) 
					{
						drawLiveCell(x, y);
						newMap[x][y] = true;
						++stats.aliveCells;
					}
					else 
						newMap[x][y] = false;
				}
				else 
				{
					if (neighbors < 2 || neighbors > 3) 
					{
						drawDeadCell(x, y)
						newMap[x][y] = false;
					}
					else 
					{
						newMap[x][y] = true;
						++stats.aliveCells;
					}
				}
			}
		}
		map = newMap;
		
		++stats.step;
		if (options.onStep)
			options.onStep(stats.step);
		if (options.onStatsUpdate)
			options.onStatsUpdate ();	
	}
	
	/* Czyścimy mape */
	this.clear = function() {
		stats.step = 0;
		stats.aliveCells = 0;
		eachCell(function(x, y) {
			initMap[x][y] = map[x][y] = false;
		});
		
		$("td").removeClass("life");
		options.onStep(stats.step);
		options.onStatsUpdate ();	
	}
	
	/* Zwraca wartość opcji @name */
	this.getOption = function(name) {
		return options[name];
	}
	
	this.setOptions = function(newOptions) {
		$.extend(options, newOptions);
		$(playground).children().remove();
		
		this.init(playground);
	}

	/* Zwraca statystyki aktualnego kroku */ 
	this.getStats = function () {
		return stats;
	}
			
	/* Przywca stan początkowy układu */
	this.backToInit = function() {
		stats.aliveCells = 0;
		eachCell(function(x, y) {
			setCellState (x, y, initMap[x][y]);
			if (initMap[x][y])
				++stats.aliveCells;
		});
		
		stats.step = 0;		
		if (options.onStep)
			options.onStep(stats.step);
		if (options.onStatsUpdate)
			options.onStatsUpdate ();			
	}
	
	/* Wyśrodkowuje wzór */
	this.centerPattern = function () {
		var edges = getEdges (map);
		
		var	w = Math.abs (edges.ymin - edges.ymax) + 1;
		var	h = Math.abs (edges.xmin - edges.xmax) + 1; 		
		var xmod = Math.floor (options.rows / 2) - Math.floor (h / 2) - edges.xmin;
		var ymod = Math.floor (options.cols / 2) - Math.floor (w / 2) - edges.ymin;

		$("td", playground).removeClass ("life");

		/* Alokujemny miejsce na nową mape */
		var newMap = new Array (options.rows);
		for (var i = 0; i <= options.rows; ++i)
			newMap[i] = new Array (options.cols);
				
		for (var x = edges.xmin; x <= edges.xmax; ++x) 
			for (var y = edges.ymin; y <= edges.ymax; ++y) 
			{
				newMap[x + xmod][y + ymod] = map[x][y];
				if (map[x][y])
					drawLiveCell (x + xmod, y + ymod);
				else 
				 	drawDeadCell (x + xmod, y + ymod);
			}
			
		map = newMap;			
	}
	
	this.load = function(data, type) {
		/* Wczytuje wzór z "czytego" tekstu */
		var loadFormPlaintext = function(data) {
			/* Usuwamy komentarze i z puste linie */
			data = data.replace(/!.*\n/, "");
			data = data.replace(/^\n/, "");
			
			/* Wczytywany wiersz i kolumna ... */
			var x = 0;
			var y = 0;
			
			for (var i = 0; i != data.length; ++i) 
			{
				if (data[i] == '\n') 
				{
					++x;
					y = 0;
				}
				else if (data[i] == "O" || data[i] == ".") 
				{
					setCellState(x, y, data[i] == "O");
					++y;
				}
			}
		}
		
		/* Wczytuje z formatu Life (*.life, *.lif) */
		var loadFromLife = function(data) {
		
			/* Deklaracja klasy do współrzednych */
			var Point = function() {
				this.x = 0;
				this.y = 0;
				
				this.setFromString = function(s) {
					xParsed = false;
					
					s = s.split(" "); //TODO: zamienić na regexpa
					for (var i = 0; i != s.length; ++i) 
					{
						if (parseInt(s[i])) 
						{
							if (!xParsed) 
							{
								this.x = parseInt(s[i]);
								xParsed = true;
							}
							else 
							{
								this.y = parseInt(s[i]);
								break;
							}
						}
					}
				}
			}
			
			var lines = data.split("\n");
			
			/* Detekcja formatu... */
			if (lines[0] == "#Life 1.05") 
			{
				/* Współrzędne lewego górnego rogu bloku */
				var blockStart = new Point();
				/* Przesuniecie kolumny, wiersza względem poczatku bloku */
				var columnMod = 0;
				var rowMod = 0;
				
				for (var i = 1; i != lines.length; ++i) 
				{
					lines[i] = $.trim(lines[i]);
					switch (lines[i][1]) {
						case "D":{ /* Opisy pomijamy... */
						} break;
						case "R":{ //TODO: Uwzgędnianie zmiany zasad 
						} break;
						case "N":{ /* Domyślne reguły gry... */
						} break;
						case "P":{ /* Deklaracja bloku */
							blockStart.setFromString(lines[i]);
							columnMod = 0;
							rowMod = 0;
						} break;
						default: {			
							/* Komórka żywa lub martwa */
							for (var j = 0; j != lines[i].length; ++j) 
							{
								var cellLive;
								if (lines[i][j] == "*") 
									cellLive = true;
								else if (lines[i][j] == ".") cellLive = false;
								
								/* Sprawdzamy czy współrzędne sa poprawne */
								var xTrue = (blockStart.x + columnMod >= 0) && (blockStart.x + columnMod <= options.cols);
								var yTrue = (blockStart.y + rowMod >= 0) && (blockStart.y + rowMod <= options.rows);
								if (xTrue && yTrue) 
									setCellState(blockStart.y + rowMod, blockStart.x + columnMod, cellLive);
								
								++columnMod;
							}
							columnMod = 0;
							++rowMod;
						}
					}
				}
			}
			else if (lines[0] == "#Life 1.06") 
			{
				for (var i = 1; i != lines.length; ++i) 
				{
					var liveCell = new Point();
					liveCell.setFromString(lines[i]);
					/* Sprawdzanie poprawności współrzędnych */
					if (liveCell.x >= 0 && liveCell.x <= options.cols &&
						liveCell.y >= 0 && liveCell.y <= options.rows) 
					{
						map[liveCell.x][liveCell.y] = true;
						createCell(liveCell.x, liveCell.y);
					}
				}
			}
			
			return true;
		}
		
		/* Wczytuje z formalu "Run Length Encoded" (*.rle) */
		var loadFromRLE = function(data) {
						
			var isNum = function (c) {				
				return (c >= '0' && c <= '9');
				//return (/[0-9]/.test (c));  
			}
			
			var isWhiteChar = function (c) {
				return (c == " " || c == "\n" || c == "\t")
			}			
						
			var row = 0; var col = 0;
			var repc = 1;
			for (var i = 0; i <= data.length - 1;) 
			{	
				/* Pomijamy początkowe białe znaki */
				while (isWhiteChar (data[i])) ++i; 								
				
				switch (data[i]) {
					case "#": { /* Komentarze */
						while (data[i] != "\n") 
							++i;
					} break;
					case "x": { /* Nagłówek */
						i = data.indexOf ("\n", i); 						
					} break;
					case "o":   /* Komórka */
					case "b": {
						//console.log (repc + "" + data[i]);
						for (; repc != 0; --repc, ++col) 
							setCellState(row, col, data[i] == 'o');						
						repc = 1;
						
						++i;						
					} break;
					case "$": { /* EOL */
						for (; repc != 0; --repc, ++col)
 							++row;
						repc = 1;
						col = 0;
						
						++i; 
					} break;
					case "!": { /* Koniec wzoru */
						return ;
					} break;
					default: {
						repc = -1;						
						while (isNum (data[i])) {
							if (repc == -1) 
								repc = parseInt(data[i]);
							else 
								repc = 10 * repc + parseInt(data[i]); 	
								
							++i;								
						}
					}	
				} 
			}
		} 
		
		if (stats.step != 0) 			
			return false;
			
		switch (type) {
			case "life": {
				loadFromLife(data);
			} break;
			case "rle": {
				loadFromRLE(data);
			} break;
			default: {
				loadFormPlaintext(data);
			}
		}
		stats.update ();
	}
	
	this.save = function(type, step) {		
		/* Zapisuje jako "plaintext" */
		var saveAsPlaintext = function(map) {
			var edges = getEdges(map);
			
			/* Przekształcamy do formatu "Plaintext" */
			var output = "";
			for (var x = edges.xmin; x <= edges.xmax; ++x) 
			{
				for (var y = edges.ymin; y <= edges.ymax; ++y) 
					output += (map[x][y]) ? "O" : ".";
				
				output += "\n";
			}
			
			return output;
		}
		
		/* Kompresuje do formatu RLE z "plaintext" */
		var compress2rle = function(data) {
			var output = "";
			
			if (data.length == 0) 				
				return output;
			
			var w = 0, h = 0;
			for (var i = 0; i != data.length - 1; ++i) 
			{
				var c = data[i];
				var repc = 1;
				
				if (c == "\n") 
				{
					/* Zliczamy puste wiersze... */
					var back = 0;
					while (data[i + 1] == "\n" || data[i + 1] == ".") {
						if (data[i + 1] == "\n") 
						{
							++repc;
							back = 0;
						}
						else
							++back; 
						++i;  
					}
					i -= back;
								
					if (repc > 1) 
						output += repc;					
					output += "$";
					
					if (w == 0) 
						w = i - 1;
					++h;
				}
				else 
				{
					while (c == data[i + 1]) 
					{
						++repc;
						++i;
					}
					
					if (c == "." && data[i + 1] == "\n") continue;
					
					if (repc > 1) output += repc;
					output += (c == ".") ? "b" : "o";					
				}
			}
			
			return "x = " + (w + 1) + ", y = " + (h + 1) + "\n" + output + "!";
		}
		
		var output = (stats.step) ? saveAsPlaintext (map) : saveAsPlaintext (initMap);		
		return (type == "plaintext") ? output : compress2rle (output);				
	}	
}
