/***************************************************************************
 *   Copyright (C) 2004 by David Sansome                                   *
 *   me@davidsansome.com                                                   *
 *                                                                         *
 *   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 2 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, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <stdlib.h>
#include <kapplication.h>
#include <kmessagebox.h>
#include <klocale.h>
#include <kconfig.h>
#include <qcheckbox.h>
#include <qcolor.h>
#include <kglobal.h>
#include <sys/time.h> 
#include <qtimer.h>
#include <kconfig.h>
#include <qlayout.h> 
#include <math.h>
#include <qlabel.h> 
#include "plasma.h"


extern "C"
{
	const char *kss_applicationName = "plasma.kss";
	const char *kss_description = I18N_NOOP( "Plasma" );
	const char *kss_version = "0.1";
	
	KPlasmaScreenSaver *kss_create( WId id )
	{
		return new KPlasmaScreenSaver( id );
	}
	
	QDialog *kss_setup()
	{
		return new KPlasmaSetup();
	}
}


KPlasmaScreenSaver::KPlasmaScreenSaver( WId id )
 : KScreenSaver( id )
{
	readSettings();
	
	_effect = new PlasmaWidget( NULL, 0, dZoom, dFocus, dSpeed, dResolution);
	embed( _effect );
	_effect->show();
}

KPlasmaScreenSaver::KPlasmaScreenSaver( WId id, int z, int f, int s, int r )
 : KScreenSaver( id ),
   dZoom(z),
   dFocus(f),
   dSpeed(s),
   dResolution(r)
{
	_effect = new PlasmaWidget( NULL, 0, dZoom, dFocus, dSpeed, dResolution);
	embed( _effect );
	_effect->show();
}



KPlasmaScreenSaver::~KPlasmaScreenSaver()
{
	delete _effect;
}


void KPlasmaScreenSaver::readSettings()
{
	KConfig* config = KGlobal::config();
	config->setGroup("Settings");
	
	dZoom = config->readNumEntry( "Zoom", 10 );
	dFocus = config->readNumEntry( "Focus", 30 );
	dSpeed = config->readNumEntry( "Speed", 20 );
	dResolution = config->readNumEntry( "Resolution", 25 );
}


//----------------------------------------------------------------------------


PlasmaWidget::PlasmaWidget( QWidget* parent, const char* name, int zoom, int focus, int speed, int res )
 : QGLWidget(parent, name), texsize(256),
   dZoom(zoom), dFocus(focus), dSpeed(speed), dResolution(res)
{
	struct timeval now;
	gettimeofday (&now, NULL);
	srand((now.tv_sec & 65535) * 1000 + now.tv_usec / 1000);
	
	_timer = new QTimer( this );
	_frameTime = 1000 / 60;
	connect( _timer, SIGNAL(timeout()), this, SLOT(nextFrame()) );
}


PlasmaWidget::~PlasmaWidget()
{
}


void PlasmaWidget::paintGL()
{
	int i, j;
	float rgb[3];
	float temp;
	static float focus = float (dFocus) / 50.0f + 0.3f;
	static float maxdiff = 0.004f * float (dSpeed);
	static int index;

	// Update constants
	for (i = 0; i < NUMCONSTS; i++) {
		ct[i] += cv[i];
		if (ct[i] > PIx2)
			ct[i] -= PIx2;
		c[i] = sin (ct[i]) * focus;
	}

	// Update colors
	for (i = 0; i < plasmaHeight; i++) {
		for (j = 0; j < plasmaWidth; j++) {
			// Calculate vertex colors
			rgb[0] = plasma[i][j][0];
			rgb[1] = plasma[i][j][1];
			rgb[2] = plasma[i][j][2];
			plasma[i][j][0] = 0.7f * (c[0] * position[i][j][0] + c[1] * position[i][j][1]
						  + c[2] * (position[i][j][0] * position[i][j][0] + 1.0f)
						  + c[3] * position[i][j][0] * position[i][j][1]
						  + c[4] * rgb[1] + c[5] * rgb[2]);
			plasma[i][j][1] = 0.7f * (c[6] * position[i][j][0] + c[7] * position[i][j][1]
						  + c[8] * position[i][j][0] * position[i][j][0]
						  + c[9] * (position[i][j][1] * position[i][j][1] - 1.0f)
						  + c[10] * rgb[0] + c[11] * rgb[2]);
			plasma[i][j][2] = 0.7f * (c[12] * position[i][j][0] + c[13] * position[i][j][1]
						  + c[14] * (1.0f - position[i][j][0] * position[i][j][1])
						  + c[15] * position[i][j][1] * position[i][j][1]
						  + c[16] * rgb[0] + c[17] * rgb[1]);

			// Don't let the colors change too much
			temp = plasma[i][j][0] - rgb[0];
			if (temp > maxdiff)
				plasma[i][j][0] = rgb[0] + maxdiff;
			if (temp < -maxdiff)
				plasma[i][j][0] = rgb[0] - maxdiff;
			temp = plasma[i][j][1] - rgb[1];
			if (temp > maxdiff)
				plasma[i][j][1] = rgb[1] + maxdiff;
			if (temp < -maxdiff)
				plasma[i][j][1] = rgb[1] - maxdiff;
			temp = plasma[i][j][2] - rgb[2];
			if (temp > maxdiff)
				plasma[i][j][2] = rgb[2] + maxdiff;
			if (temp < -maxdiff)
				plasma[i][j][2] = rgb[2] - maxdiff;

			// Put colors into texture
			index = (i * texsize + j) * 3;
			plasmamap[index] = fabstrunc (plasma[i][j][0]);
			plasmamap[index + 1] = fabstrunc (plasma[i][j][1]);
			plasmamap[index + 2] = fabstrunc (plasma[i][j][2]);
		}
	}

	// Update texture
	glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, plasmaWidth, plasmaHeight, GL_RGB, GL_FLOAT, plasmamap);

	// Draw it
	glBegin (GL_TRIANGLE_STRIP);
	glTexCoord2f (0.0f, 0.0f);
	glVertex2f (0.0f, 0.0f);
	glTexCoord2f (0.0f, texright);
	glVertex2f (1.0f, 0.0f);
	glTexCoord2f (textop, 0.0f);
	glVertex2f (0.0f, 1.0f);
	glTexCoord2f (textop, texright);
	glVertex2f (1.0f, 1.0f);
	glEnd ();
}


void PlasmaWidget::resizeGL( int w, int h )
{
	// Window initialization
	glViewport (0, 0, w, h);

	glMatrixMode (GL_PROJECTION);
	glLoadIdentity ();
	gluOrtho2D (0.0f, 1.0f, 0.0f, 1.0f);
	glMatrixMode (GL_MODELVIEW);
	glLoadIdentity ();
}


// Window initialization
void PlasmaWidget::initializeGL()
{
	int i, j;
	resizeGL(width(), height());
	

	aspectRatio = float (width()) / float (height());

	if (aspectRatio >= 1.0f) {
		wide = 30.0f / float (dZoom);

		high = wide / aspectRatio;
	} else {
		high = 30.0f / float (dZoom);

		wide = high * aspectRatio;
	}

	// Set resolution of plasma
	if (aspectRatio >= 1.0f)
		plasmaHeight = (dResolution * MAXTEXSIZE) / 100;
	else
		plasmaHeight = int (float (dResolution * MAXTEXSIZE) * aspectRatio * 0.01f);

	plasmaWidth = int (float (plasmaHeight) / aspectRatio);

	// Set resolution of texture
	texsize = 16;
	if (aspectRatio >= 1.0f)
		while (plasmaHeight > texsize)
			texsize *= 2;
	else
		while (plasmaWidth > texsize)
			texsize *= 2;

	// The "- 1" cuts off right and top edges to get rid of blending to black
	texright = float (plasmaHeight - 1) / float (texsize);
	textop = texright / aspectRatio;

	// Initialize memory and positions
	plasmamap = new float[texsize * texsize * 3];

	for (i = 0; i < texsize * texsize * 3; i++)
		plasmamap[i] = 0.0f;
	plasma = new float **[plasmaHeight];
	position = new float **[plasmaHeight];

	for (i = 0; i < plasmaHeight; i++) {
		plasma[i] = new float *[plasmaWidth];
		position[i] = new float *[plasmaWidth];

		for (j = 0; j < plasmaWidth; j++) {
			plasma[i][j] = new float[3];
			position[i][j] = new float[2];

			plasma[i][j][0] = 0.0f;
			plasma[i][j][1] = 0.0f;
			plasma[i][j][2] = 0.0f;
			position[i][j][0] = float (i * wide) / float (plasmaHeight - 1) - (wide * 0.5f);
			position[i][j][1] = float (j * high) / (float(plasmaHeight) / aspectRatio - 1.0f) - (high * 0.5f);
		}
	}
	// Initialize constants
	for (i = 0; i < NUMCONSTS; i++) {
		ct[i] = myRandf (PIx2);
		cv[i] = myRandf (0.005f * dSpeed) + 0.0001f;
	}

	// Make texture
	glBindTexture (GL_TEXTURE_2D, 1);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexImage2D (GL_TEXTURE_2D, 0, 3, texsize, texsize, 0, GL_RGB, GL_FLOAT, plasmamap);
	glEnable (GL_TEXTURE_2D);
	glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
	glPixelStorei (GL_UNPACK_ROW_LENGTH, texsize);
	
	_timer->start( _frameTime, true );
}


void PlasmaWidget::nextFrame()
{
	updateGL();
	_timer->start( _frameTime, true );
}

//----------------------------------------------------------------------------


KPlasmaSetup::KPlasmaSetup( QWidget* parent, const char* name )
        : KDialogBase( parent, name, true, i18n("Setup Plasma Screen Saver"),
          Ok|Cancel|Help|Default|Apply, Ok, true )
{
	setButtonText( Help, i18n( "A&bout" ) );
	
	QWidget *main = makeMainWidget();
	
	QHBoxLayout* top    = new QHBoxLayout(main, 0, spacingHint());
	
	setupWidget = new PlasmaSetup(main);
	slotDefault();
	top->addWidget(setupWidget);
	
	// Preview
	preview = new QWidget( main );
	preview->setFixedSize( 220, 170 );
	preview->setBackgroundColor( black );
	preview->show();    // otherwise saver does not get correct size
	_saver = new KPlasmaScreenSaver( preview->winId() );
	top->addWidget(preview);
	
	// Now that we have _saver...
	setupWidget->zoomSlider->setValue( _saver->zoom() );
	setupWidget->focusSlider->setValue( _saver->focus() );
	setupWidget->speedSlider->setValue( _saver->speed() );
	setupWidget->resolutionSlider->setValue( _saver->resolution() );
	
	setMinimumSize( sizeHint() );
	
	timer = new QTimer(this);
	connect(timer, SIGNAL(timeout()), SLOT(updatePreview()));
	
	connect(setupWidget->zoomSlider, SIGNAL(valueChanged(int)), SLOT(resetTimer()));
	connect(setupWidget->focusSlider, SIGNAL(valueChanged(int)), SLOT(resetTimer()));
	connect(setupWidget->speedSlider, SIGNAL(valueChanged(int)), SLOT(resetTimer()));
	connect(setupWidget->resolutionSlider, SIGNAL(valueChanged(int)), SLOT(resetTimer()));
}


KPlasmaSetup::~KPlasmaSetup()
{
	delete _saver;
}


void KPlasmaSetup::slotHelp()
{
	KMessageBox::about(this, 
		i18n("<h3>Plasma 1.0</h3>\n<p>Copyright (c) 2004 Terence M. Welsh<br>\n<a href=\"http://www.reallyslick.com/\">http://www.reallyslick.com/</a></p>\n\n<p>Ported to KDE by David Sansome</p>"),
		QString::null, KMessageBox::AllowLink);
}


/**
  Ok pressed - save settings and exit
*/
void KPlasmaSetup::slotOk()
{
	saveSettings();
	accept();
}

void KPlasmaSetup::slotApply()
{
	saveSettings();
	updatePreview();
}

void KPlasmaSetup::saveSettings()
{
	KConfig* config = KGlobal::config();
	config->setGroup("Settings");
	
	config->writeEntry("Zoom", setupWidget->zoomSlider->value() );
	config->writeEntry("Focus", setupWidget->focusSlider->value() );
	config->writeEntry("Speed", setupWidget->speedSlider->value() );
	config->writeEntry("Resolution", setupWidget->resolutionSlider->value() );
	
	config->sync();
}

void KPlasmaSetup::updatePreview()
{
	delete _saver;
	_saver = new KPlasmaScreenSaver( preview->winId(), setupWidget->zoomSlider->value(),
							   setupWidget->focusSlider->value(),
							   setupWidget->speedSlider->value(),
							   setupWidget->resolutionSlider->value());
}

void KPlasmaSetup::resetTimer()
{
	timer->start(750, true);
}

void KPlasmaSetup::slotDefault()
{
	setupWidget->zoomSlider->setValue( 10 );
	setupWidget->focusSlider->setValue( 30 );
	setupWidget->speedSlider->setValue( 20 );
	setupWidget->resolutionSlider->setValue( 25 );
}

#include "plasma.moc"
