// Randlebrot - fractal renderer that renders both Mandelbrot and Julia fractals
// Written by Reece Sheppard [reecesheppard@gmail.com] AKA flibX0r
// No stealzing! But feel free to is it in your projects or just for learning
// Just send me an email first, I'd love to hear what it's used for

#include <DarkGDK.h>
//#include "core_defines.h"

// Initial loop iterations (ie. resolution)
#define LOOP_COUNT			128
// How much the loop count increases when we zoom
#define RES_FACTOR			4
// How much the viewport scales when we zoom
#define ZOOM_FACTOR			8.0
// How often we sync the rendering with the screen, as number of vertical lines
#define SYNC_RATE			1

// Function declarations
inline DWORD GetPreetyColour( int a, int x, int y );
double GetCoordinate( int pos, int iMin, int iMax, double fMin, double fMax );

void DarkGDK ( void )
{
	// Mmm, lots of tasty variables
	// Z Complex number, calculates the fractal through the loop
	double Zi, Zr;
	// C Complex number, constant number for the fractal
	double Cr, Ci;
	// T Complex number, temporary storage so we don't corrupt Z whilst using it
	double Ti, Tr;
	// J Complex number, for storing the Julia fractal constant
	double Ji = 0.0, Jr = 0.0;
	// Array for storing the viewport dimensions
	double viewport[4];
	// Temporary storage for inside the loop to prevent multiple calculations
	double rSquared, iSquared;
	// Purely cosmetic variable, used to tell the user how zoomed we are
	double totalZoomage = 1;
	// X coord, Y coord, survived loop iterations, total loop iterations
	int x, y, a, loop = LOOP_COUNT;
	// Self explanitory boolean variables
	bool areWeStillLooping, isMandelbrot;
	// Buffer string for use with sprintf to produce textual output
	char *buffer = new char[128];

	// Set up DarkGDK
	// Screen resolution not used as a constant anyware, can be changed at will
	dbSetDisplayMode( 800, 600, 32 );
	dbSetWindowOn();
	// Default window position kept being off my laptops screen
	dbSetWindowPosition( 50, 50 );
	dbSyncOn();
	dbSyncRate( 0 ); // Apparently useless
	dbSync();

	// Set mandelbrot starting viewport
	// At a 4:3 ratio, alter this for different ratios (ie. fullscreen on widescreen)
	viewport[0] = -2.5;
	viewport[1] = 1.5;
	viewport[2] = -1.5;
	viewport[3] = 1.5;

	// Start as Mandelbrot
	isMandelbrot = true;

	// Main loop, go around and around and around until we're told otherwise
	while( LoopGDK() && !dbEscapeKey() )
	{
		// Clear last rendering
		dbCLS( 0 );

		// Loop around screen from top to bottom, check for exit inside the loop
		for( y = 0; y <= dbScreenHeight() && LoopGDK() && !dbEscapeKey(); y++ )
		{
			// If it's a Mandelbrot
			if( isMandelbrot ) // Then C is the coordinate of the pixel being rendered
				Ci = GetCoordinate( y, 0, dbScreenHeight(), viewport[2], viewport[3] );
			else // Otherwise it's the constant for the Julia fractal
				Ci = Ji;
			
			// Loop around screen from left to right, no exit check, fast enough to not need one
			for( x = 0; x <= dbScreenWidth(); x++ )
			{
				// See above, or search the interwebs for Mandelbrot and Julia fractals
				if( isMandelbrot )
					Cr = GetCoordinate( x, 0, dbScreenWidth(), viewport[0], viewport[1] );
				else
					Cr = Jr;
				
				// Regardless for fractal type, Z is the coordinate of the pixel being rendered
				Zi = GetCoordinate( y, 0, dbScreenHeight(), viewport[2], viewport[3] );;
				Zr = GetCoordinate( x, 0, dbScreenWidth(), viewport[0], viewport[1] );

				// Early exit variable, set to true before the loop
				areWeStillLooping = true;

				// Fractal calculation
				// Basic math is Z = Z*Z + C
				for( a = 0; a<loop && areWeStillLooping; a++ )
				{
					rSquared = Zr*Zr;
					iSquared = Zi*Zi;

					// Check for exit off to infinity
					if( rSquared+iSquared > 4.0f )
					{
						areWeStillLooping = false;
					}
					else
					{
						Tr = (rSquared + Cr ) - iSquared;
						Ti = (2.0*Zr*Zi) + Ci;
						Zr = Tr;
						Zi = Ti;
					}
				}

				// Get a preety colour for the pixel we've calculator
				dbInk( GetPreetyColour( a, x, y ), 0 );
				// Box is much faster than Dot
				dbBox( x, y, x+1, y+1 );

			}

			// Should we refresh the screen?
			// Since DarkGDK seems to use a sync rate of 60, no matter what I do, I found the code runs insanely faster if
			// I only redraw the screen every few lines instead of every pixel. Updates the titlebar too
			if( !(y%SYNC_RATE) )
			{
				sprintf( buffer, "RANDelbrot - Zoomage @ %fx - Rendering @ %3.2f%%", totalZoomage, (100.0f*y/(float)dbScreenHeight()) );
				dbSetWindowTitle( buffer );
				dbSync();
			}
		}

		// Done rendering, new title
		sprintf( buffer, "RANDelbrot - Zoomage @ %fx - Interations @ %d", totalZoomage, loop );
		dbSetWindowTitle( buffer );
		
		// Reuse of variable, reset again
		areWeStillLooping = true;
		
		while( LoopGDK() && !dbEscapeKey() && areWeStillLooping)
		{
			// Calculate the coordinate of the mouse position inside the window
			// See the function definition below to find out how this works
			Zr = GetCoordinate( dbMouseX(), 0, dbScreenWidth(), viewport[0], viewport[1]);
			Zi = GetCoordinate( dbMouseY(), 0, dbScreenHeight(), viewport[2], viewport[3]);

			// If we click the left mouse button, zoom in
			if( dbMouseClick() == 1 )
			{
				// Calulate the half width and height of the new viewpoint
				Tr = (viewport[1] - viewport[0]) / (ZOOM_FACTOR * 2.0);
				Ti = (viewport[3] - viewport[2]) / (ZOOM_FACTOR * 2.0);
				totalZoomage *= ZOOM_FACTOR;

				// T is half width so we can do this to get a new viewpoint
				viewport[0] = Zr - Tr;
				viewport[1] = Zr + Tr;
				viewport[2] = Zi - Ti;
				viewport[3] = Zi + Ti;

				// Exit this loop, go around and render the new fractal
				areWeStillLooping = false;
			}
			else
			{
				// Else if right mouse we zoom out
				if( dbMouseClick() == 2 )
				{
					// As about, except zooms out. Again, needs half size
					Tr = (viewport[1] - viewport[0]) * ZOOM_FACTOR * 0.5;
					Ti = (viewport[3] - viewport[2]) * ZOOM_FACTOR * 0.5;
					totalZoomage /= ZOOM_FACTOR;

					viewport[0] = Zr - Tr;
					viewport[1] = Zr + Tr;
					viewport[2] = Zi - Ti;
					viewport[3] = Zi + Ti;

					areWeStillLooping = false;
				}
				else
				{
					// Otherwise, if we clicked, but it wasn't the left or right mouse button
					// we then alternate which fractal we're rendering
					if( dbMouseClick() )
					{
						// Alternate between mandelbrot and julia fractals
						if( isMandelbrot )
						{
							// Make it a julia fractal
							isMandelbrot = false;
							// Set J constant from mouse position
							Jr = Zr;
							Ji = Zi;
							// Reset the viewport, centered on (0,0), not the same as the Mandelbrot viewport
							viewport[0] = -2.0;
							viewport[1] = 2.0;
							viewport[2] = -1.5;
							viewport[3] = 1.5;
						}
						else
						{
							// Make it a mandelbrot
							isMandelbrot = true;
							// Reset the viewport as the original Mandelbrot viewport
							viewport[0] = -2.5;
							viewport[1] = 1.5;
							viewport[2] = -1.5;
							viewport[3] = 1.5;
						}
						
						// Stop looping
						areWeStillLooping = false;
						// Reset the zoomage and iterations, since we've reset the viewports
						totalZoomage = 1.0;
						loop = LOOP_COUNT;
					}
				}
			}

			// Print out user data in the top left corner
			dbInk(0,0);
			dbBox(0,0,300,80);

			dbInk( dbRGB(255,255,255), 0 );
			dbText( 2, 2, "Left click to zoom in\nRight click to zoom out\nOther click to alternate fractal" );

			sprintf( buffer, "Mouse @ %f + %fi", Zr, Zi );
			dbText( 2, 60, buffer );

			dbSync();
		}

		loop *= RES_FACTOR;
	}
}

// Calculates a pretty colour
// Uses modulus to prevent overflow, and provides those nice colour layers
// Alter this function at will to make much preetyness
inline DWORD GetPreetyColour( int a, int x, int y )
{
	return (a>=LOOP_COUNT) ? 0 : dbRGB( (a%LOOP_COUNT), (a%16)<<4, 128 );	
}

// Calculates the coordinate from a position in a defined integer space to
// a defined floating point space. Somewhat optimised so it's a little hard
// to read properly
double GetCoordinate( int pos, int iMin, int iMax, double fMin, double fMax )
{
	// Set fMax to be the width of the floating-point space
	fMax -= fMin;
	// Divide fMax by the width of the integer space, turning it into a value
	// that defines the scale difference between integer and floating-point space
	fMax /= (double)(iMax - iMin);
	// Multiply the position by the scalar fMax, and then offset it by the lower
	// boundary of the floating-point space, and return the value
	return ( (double)pos * fMax ) + fMin;
}
