#!/usr/bin/perl
# Michael C. Toren <mct@toren.net>
# vim:set ts=4 sw=4 ai nobackup nowrap:

use OpenGL;
use IO::Select;
use strict;
use warnings;

# RGBA texturemap values.   Endian specific? :(
use constant VACUUM => 0x00000000;
use constant VAPOR => 0x00FF0000;
use constant ICE => 0xFFFFFF00;

my ($sizepower, $sizerows, $sizecols, $sizevec, $humidity, $ice,
	$universe, $alternateuniverse, $t, $done, $pid,
	$imagesize, @images);

sub usage { die "Usage: $0 [exponent] [humidity] [ice]\n"; }

sub isodd ($)	{ ($_[0] % 2) == 1 }
sub iseven ($)	{ ($_[0] % 2) == 0 }

sub coord2vec { $_[0] * $sizecols + $_[1] }
sub vec2coord { int($_[0] / $sizecols), ($_[0] % $sizecols) }

sub getcell { vec (${$_[0]}, $_[1], 32) }
sub setcell { vec (${$_[0]}, $_[1], 32) = $_[2] }

sub _neighbor
{
	my ($r, $c) = vec2coord shift;
	$r = ($r + shift) % $sizerows;
	$c = ($c + shift) % $sizecols;
	return coord2vec $r, $c;
}

sub north($) { _neighbor shift, -1,  0 }
sub south($) { _neighbor shift,  1,  0 }
sub east($)  { _neighbor shift,  0,  1 }
sub west($)  { _neighbor shift,  0, -1 }

sub neighborhood
{
	my $v = shift;
	my ($r, $c) = vec2coord $v;

	return ($v, south $v, south east $v, east $v) if (iseven $r+$t && iseven $c+$t);
	return ($v, south $v, south west $v, west $v) if (iseven $r+$t && isodd  $c+$t);
	return ($v, north $v, north east $v, east $v) if (isodd  $r+$t && iseven $c+$t);
	return ($v, north $v, north west $v, west $v) if (isodd  $r+$t && isodd  $c+$t);
}

sub rotate
{
	if (int rand 2) { unshift @_, pop @_ }
	else { push @_, shift @_ }
	return @_;
}

sub tick
{
	my $done = 1;

	for (my $r = $t % 2; $r < $sizerows; $r += 2)
	{
		for (my $c = $t % 2; $c < $sizecols; $c += 2)
		{
			my $v = coord2vec $r, $c;
			my @n = neighborhood $v;

			if (grep { getcell(\$universe, $_) == ICE } @n)
			{
				for my $i (@n)
				{
					my $cell = getcell \$universe, $i;
					setcell \$alternateuniverse, $i, 
						($cell == VAPOR ? ICE : $cell);
				}
				next;
			}

			my @foo;

			if (grep { getcell(\$universe, $_) == VAPOR } @n) {
				@foo = rotate @n;
				$done = 0;
			} else {
				@foo = @n;
			}

			setcell \$alternateuniverse, $foo[$_],
				getcell \$universe, $n[$_]
					for (0 .. $#foo);
		}
	}

	return $done;
}

# This is a mess.  The first set of arguments is either an RGB color
# to use for drawing the polygon, or a texture map index number plus a
# set of four coordinates for mapping the texture to the polygon.  The
# second set of coordinates are the points for the polygon.  Looking
# at the number of arguments determines the type of arguments being
# passed.

sub boxside
{
	my ($color, $tex, @texcoord);

	if (scalar @_ == 21)
	{
		$tex = shift;
		push @texcoord, (shift) for (1..8);
		glTexImage2D GL_TEXTURE_2D, 0, GL_RGB,
				$imagesize, $imagesize, 0, GL_RGBA,
				GL_UNSIGNED_BYTE, $images[$tex];
	}

	elsif (scalar @_ == 13)
	{
		$color = shift;
		glColor3f +($color & 0xFF0000) / 0xFF,
			($color & 0x00FF00) / 0xFF,
			($color & 0x0000FF) / 0xFF;
	}

	else { die "Invalid number of arguments for boxside(), sorry\n"; }

	glEnable GL_TEXTURE_2D if ($tex);
	glBegin GL_QUADS;

	for (1..4)
	{
		glTexCoord2f shift @texcoord, shift @texcoord if ($tex);
		glVertex3f shift, shift, shift;
	}

	glEnd;
	glDisable GL_TEXTURE_2D if ($tex);
}

sub draw
{
	my ($x, $y, $z, $size) = (0, 0, 0, 10);
	glClear GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT;

	boxside 1,  (0, 0), (1, 0), (1, 1), (0, 1),  ($x-$size, $y+$size, $z+$size), ($x-$size, $y-$size, $z+$size), ($x+$size, $y-$size, $z+$size), ($x+$size, $y+$size, $z+$size); # front
	boxside 2,  (0, 0), (1, 0), (1, 1), (0, 1),  ($x+$size, $y+$size, $z+$size), ($x+$size, $y-$size, $z+$size), ($x+$size, $y-$size, $z-$size), ($x+$size, $y+$size, $z-$size); # right
	boxside 3,  (0, 1), (1, 1), (1, 0), (0, 0),  ($x-$size, $y+$size, $z-$size), ($x-$size, $y-$size, $z-$size), ($x+$size, $y-$size, $z-$size), ($x+$size, $y+$size, $z-$size); # back
	boxside 4,  (0, 1), (1, 1), (1, 0), (0, 0),  ($x-$size, $y+$size, $z+$size), ($x-$size, $y-$size, $z+$size), ($x-$size, $y-$size, $z-$size), ($x-$size, $y+$size, $z-$size); # left
	boxside 0xFF0000,                            ($x-$size, $y+$size, $z+$size), ($x+$size, $y+$size, $z+$size), ($x+$size, $y+$size, $z-$size), ($x-$size, $y+$size, $z-$size); # top
	boxside 0xFF0000,                            ($x-$size, $y-$size, $z+$size), ($x+$size, $y-$size, $z+$size), ($x+$size, $y-$size, $z-$size), ($x-$size, $y-$size, $z-$size); # bottom

	glFlush;
	glXSwapBuffers;
}

{ my ($oldmotionx, $oldmotiony);
sub eventhandler
{
	my ($event, @data) = glpXNextEvent;

	if ($event == KeyPress)
	{
		my $key = shift @data;
		glRotatef  5, 1, 0, 0 if ($key eq "j");
		glRotatef -5, 1, 0, 0 if ($key eq "k");
		glRotatef -5, 0, 1, 0 if ($key eq "h");
		glRotatef  5, 0, 1, 0 if ($key eq "l");
		exit if (lc $key eq "q");
		return;
	}

	if ($event == ConfigureNotify)
	{
		glViewport 0, 0, @data;	# width, and height
		return;
	}

	if ($event == MotionNotify)
	{
		my ($x, $y) = @data[1,2];

		unless (defined $oldmotionx && defined $oldmotiony)
		{
			($oldmotionx, $oldmotiony) = ($x, $y);
			return;
		}

		glRotatef $x - $oldmotionx, 0, 1, 0;
		glRotatef $y - $oldmotiony, 1, 0, 0;
		($oldmotionx, $oldmotiony) = ($x, $y);
		return;
	}

	if ($event == ButtonRelease)
	{
		($oldmotionx, $oldmotiony) = (undef, undef);
		return;
	}
} }

$sizepower = shift || 7;
$sizerows = 2**$sizepower *4;
$sizecols = 2**$sizepower;
$imagesize = 2**$sizepower;
$sizevec = coord2vec $sizerows-1, $sizecols-1;
$humidity = shift;
$ice = shift;

die "Sorry, rows and columns must be even integers\n"
	unless (iseven $sizerows and iseven $sizecols);
die "Sorry, minimum value for rows and columns is 4\n"
	unless ($sizerows >= 4 and $sizecols >= 4);

$humidity = 20 unless (defined $humidity);
$ice = 1 unless (defined $ice);
$t = 0;

glpOpenWindow(width=>600,height=>600,
				mask => StructureNotifyMask | KeyPressMask
					| ButtonPressMask | ButtonReleaseMask
					| Button1MotionMask,
				attributes=>[GLX_RGBA, GLX_DOUBLEBUFFER]);

glOrtho -20, 20, -20, 20, -20, 20;	# viewing dimensions
glClearColor .5, .5, .5, 1;			# background color
glPolygonMode GL_FRONT, GL_FILL;	# polygon fill mode
glEnable GL_DEPTH_TEST;				# enable testing for hidden lines
glMatrixMode GL_MODELVIEW;

# initial rotated display
glRotatef  5, 1, 0, 0;
glRotatef 40, 0, 1, 0;
glRotatef 10, 0, 0, 1;

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameterf GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP;
glTexParameterf GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP;
glTexParameterf GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST;
glTexParameterf GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST;
glTexEnvf GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL;

pipe R, W or die "pipe: $!\n";
die "fork: $!\n" unless defined ($pid = fork);
END { kill 9, $pid if ($pid) } 

if ($pid)
{
	close W;
	my $select = IO::Select->new(\*R);

	sub input
	{
		my $buf = 0;
		my $left = $imagesize * $imagesize * 4 * 4;

		while ($left)
		{
			my $r = sysread R, $buf, $left, length($buf); 
			die "sysread: $!\n" unless (defined $r);

			if ($r == 0)
			{
				print "Simulation complete\n";
				close R;
				return;
			}

			$left -= $r;
		}

		for my $i (0 .. 3)
		{
			my $frame = $imagesize * $imagesize * 4;
			my $offset = $frame * $i;
			$images[$i+1] = substr $buf, $offset, $frame;
		}

		warn "Clock tick ", $t++, "\n";
	}

	input;
	while(1)
	{
		eventhandler while (XPending);
		draw;
		input if ($select->can_read(0.025));
	}
}

else
{
	close R;
	select W;
	$|=1;

	sub output
	{
		print W $universe;
	}

	# initialize the universe.
	$universe = $alternateuniverse = 0;
	setcell \$universe, $_, (rand 100 < $humidity) ? VAPOR : VACUUM
		for (0..$sizevec);
	
	# if only once particle, center it on a box face
	if ($ice == 1)
	{
		setcell \$universe,
			coord2vec (int ($sizerows/8), $sizecols/2), ICE;
	} else {
		setcell \$universe,
			coord2vec (int rand $sizerows, int rand $sizecols), ICE
				for (1 .. $ice);
	}

	my $dr = $sizerows / 8;
	my $dc = $sizecols - 10;

	while (! $done)
	{
		output;
		$done = tick;
		$universe = $alternateuniverse;
		$t++;
	}
	output;
	close W;
	exit;
}


