/* MetaBalls, Copyright (c) 2002-2003 W.P. van Paassen <peter@paassen.tmfweb.nl>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or
 * implied warranty.
 *
 * Module - "metaballs.c"
 *
 * [01/24/03] - W.P. van Paassen: Additional features
 * [12/29/02] - W.P. van Paassen: Port to X for use with XScreenSaver, the shadebob hack by Shane Smit was used as a template
 * [12/26/02] - W.P. van Paassen: Creation for the Demo Effects Collection (http://demo-effects.sourceforge.net)
 */

#include <math.h>
#include "screenhack.h"
#include <X11/Xutil.h>

/*#define VERBOSE*/ 

char *progclass = "Metaballs";

char *defaults [] = {
  ".background: black",
  ".foreground: white",
  "*color:    random",
  "*count:    10",
  "*cycles:   1000",
  "*ncolors:  256",
  "*delay:    5000",
  "*radius:   100",
  "*delta:   3",
  0
};

XrmOptionDescRec options [] = {
  { "-color",   ".color",   XrmoptionSepArg, 0 },
  { "-ncolors", ".ncolors", XrmoptionSepArg, 0 },
  { "-count",   ".count",   XrmoptionSepArg, 0 },
  { "-delay",   ".delay",   XrmoptionSepArg, 0 },
  { "-cycles",  ".cycles",  XrmoptionSepArg, 0 },
  { "-radius",  ".radius",  XrmoptionSepArg, 0 },
  { "-delta",  ".delta",  XrmoptionSepArg, 0 },
  { 0, 0, 0, 0 }
};

static unsigned short iWinWidth, iWinHeight;
static char *sColor;

#define RANDOM() ((int) (random() & 0X7FFFFFFFL))

/*blob structure*/
typedef struct 
{
  short xpos,ypos;
} BLOB;

static unsigned char nBlobCount;
static unsigned char radius;
static unsigned char delta;
static unsigned char dradius;
static unsigned short sradius;
static unsigned char **blob;
static BLOB *blobs;
static unsigned char **blub;

static void init_blob(BLOB *blob)
{
  blob->xpos =  (iWinWidth>> 1) - radius;
  blob->ypos =  (iWinHeight >> 1) - radius;
}

static void Execute( Display *pDisplay,
                     Window MainWindow,
                     GC *pGC, XImage *pImage,
                     signed short iColorCount, unsigned long *aiColorVals )
{
	unsigned int i, j, k;

	/* clear blub array */
	for (i = 0; i < iWinHeight; ++i)
	  memset(blub[i], 0, iWinWidth * sizeof(unsigned char));

	/* move blobs */
	for (i = 0; i < nBlobCount; i++)
	{
	  blobs[i].xpos += -delta + (int)((delta + .5f) * (random()/(RAND_MAX+1.0)));
	  blobs[i].ypos += -delta + (int)((delta + .5f) * (random()/(RAND_MAX+1.0)));
	}

	/* draw blobs to blub array */
	for (k = 0; k < nBlobCount; ++k)
	  { 
	    if (blobs[k].ypos > -dradius && blobs[k].xpos > -dradius && blobs[k].ypos < iWinHeight && blobs[k].xpos < iWinWidth)
	      {
		for (i = 0; i < dradius; ++i)
		  {
		    if (blobs[k].ypos + i >= 0 && blobs[k].ypos + i < iWinHeight)
		      {
			for (j = 0; j < dradius; ++j)
			  {
			    if (blobs[k].xpos + j >= 0 && blobs[k].xpos + j < iWinWidth)
			      {
				if (blub[blobs[k].ypos + i][blobs[k].xpos + j] < iColorCount)
				  {
				    if (blub[blobs[k].ypos + i][blobs[k].xpos + j] + blob[i][j] > iColorCount)
				      blub[blobs[k].ypos + i][blobs[k].xpos + j] = iColorCount;
				    else 
				      blub[blobs[k].ypos + i][blobs[k].xpos + j] += blob[i][j];     
				  }
			      }
			  }
		      }
		  }
	      }
	    else
	      init_blob(blobs + k);
	  }

	memset( pImage->data, 0, pImage->bytes_per_line * pImage->height);

	/* draw blub array to screen */
	for (i = 0; i < iWinHeight; ++i)
	  {
	    for (j = 0; j < iWinWidth; ++j)
	      {
		if (aiColorVals[blub[i][j]] > 0)
		  XPutPixel( pImage, j, i, aiColorVals[blub[i][j]] );
	      }
	  }

	XPutImage( pDisplay, MainWindow, *pGC, pImage,
		   0, 0, 0, 0, iWinWidth, iWinHeight );
	XSync( pDisplay, False );
}

static unsigned long * SetPalette(Display *pDisplay, Window Win, char *sColor, signed short *piColorCount )
{
	XWindowAttributes XWinAttribs;
	XColor Color, *aColors;
	unsigned long *aiColorVals;
	signed short iColor;
	float nHalfColors;
	
	XGetWindowAttributes( pDisplay, Win, &XWinAttribs );
	
	Color.red =   RANDOM() % 0xFFFF;
	Color.green = RANDOM() % 0xFFFF;
	Color.blue =  RANDOM() % 0xFFFF;

	if( strcasecmp( sColor, "random" ) && !XParseColor( pDisplay, XWinAttribs.colormap, sColor, &Color ) )
		fprintf( stderr, "%s: color %s not found in database. Choosing to random...\n", progname, sColor );

#ifdef VERBOSE
	printf( "%s: Base color (RGB): <%d, %d, %d>\n", progclass, Color.red, Color.green, Color.blue );
#endif  /*  VERBOSE */

	*piColorCount = get_integer_resource( "ncolors", "Integer" );
	if( *piColorCount <   2 )	*piColorCount = 2;
	if( *piColorCount > 255 )	*piColorCount = 255;

	aColors    = calloc( *piColorCount, sizeof(XColor) );
	aiColorVals = calloc( *piColorCount, sizeof(unsigned long) );
	
	for( iColor=0; iColor<*piColorCount; iColor++ )
	{
		nHalfColors = *piColorCount / 2.0F;
		/* Black -> Base Color */
		if( iColor < (*piColorCount/2) )
		{
			aColors[ iColor ].red   = ( Color.red   / nHalfColors ) * iColor;
			aColors[ iColor ].green = ( Color.green / nHalfColors ) * iColor;
			aColors[ iColor ].blue  = ( Color.blue  / nHalfColors ) * iColor;
		}
		/* Base Color -> White */
		else
		{
			aColors[ iColor ].red   = ( ( ( 0xFFFF - Color.red )   / nHalfColors ) * ( iColor - nHalfColors ) ) + Color.red;
			aColors[ iColor ].green = ( ( ( 0xFFFF - Color.green ) / nHalfColors ) * ( iColor - nHalfColors ) ) + Color.green;
			aColors[ iColor ].blue  = ( ( ( 0xFFFF - Color.blue )  / nHalfColors ) * ( iColor - nHalfColors ) ) + Color.blue;
		}

		if( !XAllocColor( pDisplay, XWinAttribs.colormap, &aColors[ iColor ] ) )
		{
			/* start all over with less colors */	
			XFreeColors( pDisplay, XWinAttribs.colormap, aiColorVals, iColor, 0 );
			free( aColors );
			free( aiColorVals );
			(*piColorCount)--;
			aColors     = calloc( *piColorCount, sizeof(XColor) );
			aiColorVals = calloc( *piColorCount, sizeof(unsigned long) );
			iColor = -1;
		}
		else
			aiColorVals[ iColor ] = aColors[ iColor ].pixel;
	}

	free( aColors );

	XSetWindowBackground( pDisplay, Win, aiColorVals[ 0 ] );

	return aiColorVals;
}


static void Initialize( Display *pDisplay, Window Win, GC *pGC, XImage **ppImage )
{
	XGCValues gcValues;
	XWindowAttributes XWinAttribs;
	int iBitsPerPixel, i, j;
	unsigned int distance_squared;
	float fraction;

	/* Create the Image for drawing */
	XGetWindowAttributes( pDisplay, Win, &XWinAttribs );

	/* Find the preferred bits-per-pixel. (jwz) */
	{
		int i, pfvc = 0;
		XPixmapFormatValues *pfv = XListPixmapFormats( pDisplay, &pfvc );
		for( i=0; i<pfvc; i++ )
			if( pfv[ i ].depth == XWinAttribs.depth )
			{
				iBitsPerPixel = pfv[ i ].bits_per_pixel;
				break;
			}
		if( pfv )
			XFree (pfv);
	}

	/*  Create the GC. */
	*pGC = XCreateGC( pDisplay, Win, 0, &gcValues );

	*ppImage = XCreateImage( pDisplay, XWinAttribs.visual, XWinAttribs.depth, ZPixmap, 0, NULL,
							  XWinAttribs.width, XWinAttribs.height, BitmapPad( pDisplay ), 0 );
	(*ppImage)->data = calloc((*ppImage)->bytes_per_line, (*ppImage)->height);

	iWinWidth = XWinAttribs.width;
	iWinHeight = XWinAttribs.height;

	/*  Get the base color. */
	sColor = get_string_resource( "color", "Color" );

	/*  Get the delta. */
	delta = get_integer_resource( "delta", "Integer" );
	if (delta < 1)
	  delta = 1;
	else if (delta > 20)
	  delta = 20;
	
	/*  Get the radius. */
	radius = get_integer_resource( "radius", "Integer" );
	if (radius < 2)
	  radius = 2;
	if (radius > 100)
	  radius = 100;
	
	radius = (radius / 100.0) * (iWinHeight >> 3);

	dradius = radius * 2;
	sradius = radius * radius;

	/* create blob */
	blob = malloc ( dradius * sizeof(unsigned char*));
	for (i = 0; i < dradius; ++i)
	  blob[i] = malloc( dradius * sizeof(unsigned char));

	/* create blub array */
	blub = malloc( iWinHeight * sizeof(unsigned char*));
	for (i = 0; i < iWinHeight; ++i)
	  blub[i] = malloc( iWinWidth * sizeof(unsigned char));

	/* create blob */
	for (i = -radius; i < radius; ++i)
	  {
	    for (j = -radius; j < radius; ++j)
	      {
		distance_squared = i * i + j * j;
		if (distance_squared <= sradius)
		  {
		    /* compute density */     
		    fraction = (float)distance_squared / (float)sradius;
		    blob[i + radius][j + radius] = pow((1.0 - (fraction * fraction)),4.0) * 255.0;
		  }
		else
		  {
		    blob[i + radius][j + radius] = 0;
		  }
	      }    
	  }
	
	for (i = 0; i < nBlobCount; i++)
	  {
	    init_blob(blobs + i);
	  }
}

void screenhack(Display *pDisplay, Window Win )
{
	GC gc;
	signed short iColorCount = 0;
	unsigned long *aiColorVals = NULL;
	XImage *pImage = NULL;
#ifdef VERBOSE
	time_t nTime = time( NULL );
	unsigned short iFrame = 0;
#endif  /*  VERBOSE */
	int delay, cycles, i;

	nBlobCount = get_integer_resource( "count", "Integer" );
	if( nBlobCount > 255 ) nBlobCount = 255;
	if( nBlobCount <  2 ) nBlobCount = 2;

	if( ( blobs = calloc( nBlobCount, sizeof(BLOB) ) ) == NULL )
	{
		fprintf( stderr, "%s: Could not allocate %d Blobs\n", progclass, nBlobCount );
		return;
	}
#ifdef VERBOSE 
	printf( "%s: Allocated %d Blobs\n", progclass, nBlobCount );
#endif  /*  VERBOSE */

	Initialize( pDisplay, Win, &gc, &pImage );

	delay = get_integer_resource( "delay", "Integer" );
	cycles = get_integer_resource( "cycles", "Integer" );
	i = cycles;

	while( 1 )
	{
		screenhack_handle_events( pDisplay );

		if( i++ >= cycles )
		{
                        XWindowAttributes XWinAttribs;
                        XGetWindowAttributes( pDisplay, Win, &XWinAttribs );

			memset( pImage->data, 0, pImage->bytes_per_line * pImage->height );
                        XFreeColors( pDisplay, XWinAttribs.colormap, aiColorVals, iColorCount, 0 );
			free( aiColorVals );
			aiColorVals = SetPalette( pDisplay, Win, sColor, &iColorCount );
                        XClearWindow( pDisplay, Win );
			for (i = 0; i < nBlobCount; i++)
			  {
			    init_blob(blobs + i);
			  }
			i = 0;
		}

		Execute( pDisplay, Win, &gc, pImage, iColorCount - 1, aiColorVals );

		if( delay && !(i % 4) )
			usleep(delay);

#ifdef VERBOSE
		iFrame++;
		if( nTime - time( NULL ) )
		{
			printf( "%s: %d FPS\n", progclass, iFrame );
			nTime = time( NULL );
			iFrame = 0;
		}
#endif  /*  VERBOSE */
	}

	free( pImage->data );
	XDestroyImage( pImage );
	free( aiColorVals );
	free( blobs );
	for (i = 0; i < iWinHeight; ++i)
	  free( blub[i] );
	free( blub );
	for (i = 0; i < dradius; ++i)
	  free( blob[i] );
	free( blob );
}


/* End of Module - "metaballs.c" */

