//
// notes / improvements
// 3) change sim to allow dynamic resolution updates
//
import std.stream;
import std.stdio;
import std.c.stdlib;

import std.string;
import std.c.time;

import bcd.curses.curses;
const int ERR = 255;

alias extern (C) _win_st * curses_screen;

// names for cell states
const int LIVE=1;
const int DEAD=0;

// KEY CODES
const int TERM_UP=3;
const int TERM_DOWN=2;
const int TERM_LEFT=4;
const int TERM_RIGHT=5;
const int TERM_ESC=27;
const int TERM_ENTER=13;

// console screen size

short ROWMAX;
short COLMAX;

/* life Soup:
 *	this is the object that maintains both the displayed and 
 *	cooking version of the life experiment.  it knows how to 
 *	load a population, compute the next population and 
 *	display the current population
 */
 
class Soup
{
private:
	char[] sbuf;    // the output buffer
	int ncount;
	int rminus, rplus, cminus, cplus;
public:
	ubyte surface[][];  // the surface contents
 	int cellpop;		       // the population

	this()   // constructor
 	{
		// allocate dynamic arrays
		sbuf = new char[ROWMAX*COLMAX+1];    // the output buffer
		surface = new ubyte[][](ROWMAX,COLMAX);  // the surface contents

	    	// since D is "declaration is initialization" i dont have to set 
		// all of the cells to 'false'.  that's the default.

		// initialize the output buffer to spaces.
		for (int i=0; i<ROWMAX*COLMAX; i++) sbuf[i] = ' ';
			
		// since were passing it to curses, it has to maintain a dual
		// life as a c string, zero term it.
		sbuf[ROWMAX*COLMAX] = 0;
		
		cellpop = 0;	
	}

	void load(string filename)
	{
		// load the list of active cells
		// one cell coordinate per line
		// format is: row,column<newline>
		// rows,cols start at 0,0
		int irow, icol;
		char[][] celloc;
		
		File f = new File(filename);

		while (!f.eof())
		{
			celloc = split(f.readLine(), ",");
			irow = std.string.atoi(celloc[0]);
			icol = std.string.atoi(celloc[1]);
			surface[irow][icol] = LIVE; sbuf[irow*COLMAX+icol] = '*';
			cellpop++;
		}
		f.close();    
	}

	
	// returns the number of live neighbors
	int neighbors(int row, int col)
	{
		ncount = 0;
		
		// soup wraps around at limits (toroidal)
		rminus = row-1; if (rminus < 0) rminus = ROWMAX-1;
		rplus = row+1; if (rplus == ROWMAX) rplus = 0;
		cminus = col-1; if (cminus < 0) cminus = COLMAX-1;
		cplus = col+1; if (cplus == COLMAX) cplus = 0;

		// check for neighbors
		// since were using 0 and 1 for cell contents, we can simply add them up

		return 
		   surface[rminus][cminus] + surface[rminus][col] + surface[rminus][cplus] +
			     surface[row][cminus] + surface[row][cplus] +
		   surface[rplus][cminus] + surface[rplus][col] + surface[rplus][cplus];
		
	}

	// make a cell active
	void spawn(int row, int col)
	{
		surface[row][col] = LIVE;
		cellpop++;
	}

	// kill a cell
	void kill(int row, int col)
	{
		surface[row][col] = DEAD;
		cellpop--;
	}

	// cook up a batch of this soup from soup stock
	void cook(Soup stock)
	{
		int curNeighbors, soffset;
		
		// compute the next gen

		soffset = 0;
		cellpop = stock.cellpop; // copy pop count
		
		// we could probably do this a little more elegantly 
		// with foreach, though i'm not sure it would be more readable
		
		for (int srow=0; srow<ROWMAX; srow++) {
			for (int scol=0; scol<COLMAX; scol++) {
				// check number of neighbors in stock cell
 				curNeighbors = stock.neighbors(srow,scol);
				// is the stock cell populated?
				if (stock.surface[srow][scol]) {
					// its live, does it survive?
					if (curNeighbors<2 || curNeighbors>3) {
						// no, it dies
						surface[srow][scol] = DEAD;
						sbuf[soffset] = ' ';
						cellpop--;
					}
					else {
						// cell survives, copy it
						surface[srow][scol] = LIVE;
						sbuf[soffset] = '*';
					}
				}
				else if (curNeighbors == 3) {
					// its lifeless, but spawns
					surface[srow][scol] = LIVE;
					cellpop++;
					sbuf[soffset] = '*';
				}
				else {
					// its lifeless and stays lifeless
					// (probably not needed)
					surface[srow][scol] = DEAD;
					sbuf[soffset] = ' ';
				}
			soffset++;  // bump output buffer loc
			}
		}
	}
	
	// edit this batch of soup
	// returns true if successful edit, else 
	// user requested exit
	bool edit()
	{
		bool editing = true;
		int crow=0, ccol=0;
		char ch;
		
		nodelay(stdscr, FALSE);
		while (editing) {
			move(crow,ccol);
			ch = wgetch(stdscr);
			switch (ch) {
				case TERM_ENTER:
					// finished successful edit
					return true;
				case ' ':
					// toggle cell state
					surface[crow][ccol] =  ! surface[crow][ccol];
					if (surface[crow][ccol]) {
						cellpop++;  // bump population
						echochar('*');
					}
					else {
						cellpop--;  // reduce population
						echochar(' ');
					}
					break;
				case TERM_UP:
				case 'i':
					// cursor up
					crow--; if (crow < 0) crow = ROWMAX-1;
					break;
				case TERM_DOWN:
				case 'k':
					// cursor down
					crow++; if (crow == ROWMAX) crow = 0;
					break;
				case TERM_LEFT:
				case 'j':
					// cursor left
					ccol--; if (ccol < 0) ccol = COLMAX-1;
					break;
				case TERM_RIGHT:
				case 'l':
					// cursor right
					ccol++; if (ccol == COLMAX) ccol = 0;
					break;
				case TERM_ESC:
					// exit editor
					return false;
				default:
					break;
			}
			status();
		}
	}
				

	
	void display(curseWindow win)
	{
		// writes to default window
		move(0,0);
	        addstr(this.sbuf.ptr);
	}

	void status()
	{
		move(ROWMAX-1,COLMAX-30);
		printw("pop %d",cellpop);
	}

}

/*
 * the 'lifemachine' is the simulator object.  you could conceivably
 * run more than one at a time.  we model the life simulation as two
 * collections of 'life' soup.  the 'bowl' of soup is the one we're viewing.
 * the soup pot is (not suprisingly) where we cook up the next life
 * generation.  I could have used 'petri dishes' as the metaphor but bowl and
 * pot seemed to work 8-)
 * 
 * the lifeMachine just controls the initialization and swapping of the
 * bowl and pot at every tick.  the soup knows how to cook up a new
 * generation and display it.
 */

class LifeMachine
{
private:
	Soup bowl;	// the currently displayed life soup
	Soup pot;	// the soup thats cooking (next gen)
	Soup temp;      // for swap
	int gen;	// current generation

	curseWindow curWin;
	
public:
	this(curseWindow win)
	{
		// load the initial soup
		bowl = new Soup();
		pot = new Soup();
		
		bowl.load("life.conf");

		// remember the current window
		curWin = win;

		// reset the generation count
		gen = 0;
	}

	void status()
	{
		move(ROWMAX-1, COLMAX-40);
		printw("gen %d",gen);
	}
	
	void tick()
	{
		// display the contents of the soup bowl on the screen
		bowl.display(curWin);
		
		// update status (generation, population)
		status();
		bowl.status();
		
		refresh();
		
		// cook up the new soup
		pot.cook(bowl);
		
		// swap soups
				
		temp = bowl;
		bowl = pot;
		pot = temp;
		
		// bump generation
		gen++;

	}

	void simulate()
	{
		// the simulation loop.
		char ch;
		bool running = false;
		bool simulating = true;
		bool step = false;
		
		// since running is initially false, we start in edit mode
		while (simulating) {
			bowl.display(curWin);
			status();
			bowl.status();
			running = bowl.edit();
			if (! running) simulating = false;  // exit req from editor
			// now we're in run mode
			nodelay(stdscr, !step);  // non-blocking char i/o if not single stepping
			while (running) {
				tick();
				if (! step) usleep(31250);	// sleep momentarily
				if ((ch = wgetch(stdscr)) != ERR) {
					// user wants something
					switch (ch) {
						case TERM_ENTER:
							// enter edit mode
							running = false;
							break;
						case 27:
							// exit the sim
							running = false;
							simulating = false;
							break;
						case ' ':
							// enter/exit single step mode
							// any other char (except enter or ESC) in step mode advances
							// one generation.
							step = ! step;
							nodelay(stdscr, ! step);
						default:
							// writefln("found spurious char %d",ch);
							// exit(1);
							break;
					}
				}
			}
		}
		
	}
}

/* curseWindow: 
 *	this isn't really necessary until we make something more complex than a
 *	single window.  right now it just handles curses initialization and shutdown.
 */

class curseWindow
{
private:
	curses_screen cScreen;   // for time being just use default screen
public:
	this()
	{
		// initialize curses window
		initscr();
		cbreak();
		noecho();
		nonl();
		intrflush(stdscr,FALSE);
		keypad(stdscr, TRUE);
		
		// use the standard screen
		cScreen = stdscr;
		
		// retrieve row and column max
		ROWMAX = stdscr._maxy + 1;
		COLMAX = stdscr._maxx + 1;
	}

	~this()
	{
		// destroy and cleanup curses window.
		// we should probably explicitly return
		// window to "sane" state.
		endwin();
	}

	// some methods

	void clear()
	{
		move(0,0);
		clear();
	}

	// display a D string on it
	void show(string s)
	{
		addstr(std.string.toStringz(s));
	}

	void cursorTo(int row, int col)
	{
		move(row,col);
	}
	
	void flush()
	{
		refresh();
	}	
}


/*
 * dlife:
 *	a simple curses-based implementation of John Conway's Life.  
 *	its a double buffered implementation with no sparse-cell
 *	optimizations.
 */

int main() {
	// create a window for the simulation
	auto cWin = new curseWindow();

	// popup welcome screen
	wmove(stdscr,ROWMAX/2-1,COLMAX/2-1-9);
	waddstr(stdscr,"Conway's Life in D");
	wmove(stdscr,ROWMAX/2,COLMAX/2-17);
	waddstr(stdscr,"By Jim Burnes. v1.0 04/13/2008 ");
	refresh();
	sleep(3);
	erase();

	// create the life machine on the window
	auto life = new LifeMachine(cWin);

	// start the machine
	life.simulate();
	

	return 0;
}


