//This code was created by Jordan Isaak
//Code written in March, 2002
//My website is at members.shaw.ca/jordanisaak
//My email is jordanisaak@hotmail.com

#include <windows.h>
#include <GL/glut.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

/*Alpha texture*/
GLuint alphatexture;

/*Linear gradient that is to be stored in the alpha texture*/
unsigned char alphadata[256] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
									10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
									20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
									30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
									40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
									50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
									60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
									70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
									80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
									90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
									100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
									110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
									120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
									130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
									140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
									150, 151, 152, 153, 154, 155, 156, 157, 158, 159,
									160, 161, 162, 163, 164, 165, 166, 167, 168, 169,
									170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
									180, 181, 182, 183, 184, 185, 186, 187, 188, 189,
									190, 191, 192, 193, 194, 195, 196, 197, 198, 199,
									200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
									210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
									220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
									230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
									240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
									250, 251, 252, 253, 254, 255};

/*Clamp to edge extension stuff*/
#define GL_CLAMP_TO_EDGE_EXT 0x812F

/*Subtract blending extension stuff*/
#define GL_FUNC_ADD_EXT 0x8006
#define GL_FUNC_SUBTRACT_EXT 0x800A
#define GL_FUNC_REVERSE_SUBTRACT_EXT 0x800B

typedef void (APIENTRY * PFNGLBLENDEQUATIONEXTPROC) (int mode);
PFNGLBLENDEQUATIONEXTPROC glBlendEquationEXT;

/*Texture coordinate generation stuff*/
float eyeplane[4] = {0.0f, 0.0f, 1.0f / (-20.0f), 0.0f};

/*Lighting variables*/
float lightpos[4] = {1.0f, 1.0f, 1.0f, 0.0f};
float lightambient[4] = {0.2f, 0.2f, 0.2f, 1.0f};
float lightdiffuse[4] = {1.0f, 1.0f, 1.0f, 1.0f};
float teapotmaterial[4] = {0.0f, 0.0f, 1.0f, 1.0f};
float groundmaterial[4] = {0.0f, 0.5f, 0.0f, 1.0f};

/*Variables to control fog sphere positions*/
float sphere[4][4] = {0.0f, 0.0f, 0.0f, 2.0f,
					0.0f, 0.0f, 0.0f, 2.0f,
					0.0f, 0.0f, 0.0f, 2.0f,
					0.0f, 0.0f, 0.0f, 2.0f};

/*Keeps track of where the mouse was last*/
int lastx = 0, lasty = 0;

/*Camera control variables*/
float rotation = 0;
float elevation = 1.0f;
float cameradistance = 8.0f;
float camerapos[3] = {0.0f, 0.0f, 0.0f};

/*Controls what mouse position changes update*/
int controlmode = 0;

/*Controls what type of fog the current volume is*/
int fogtype[4] = {0, 0, 0, 0};

/*Controls whether a given fog volume is visible or not*/
int fogvisible[4] = {1, 0, 0, 0};

/*Color information for each fog volume*/
float fogcolor[4][3] = {1.0f, 1.0f, 1.0f,
						1.0f, 1.0f, 1.0f,
						1.0f, 1.0f, 1.0f,
						1.0f, 1.0f, 1.0f};

/*The fog volume that currently is under user control*/
int currentfogvolume = 0;

void menu(int selection)
{
	/*Set what mouse movement controls,
	based on what the menu selection was*/
	controlmode = selection;
}

void idle(void)
{
	glutPostRedisplay();
}

int init(void)
{
	int has_EXT_blend_subtract = 0;
	int has_EXT_texture_edge_clamp = 0;
	char extensionstring[4096];
	char *currentextension;

	//Check to see if the EXT_texture_edge_clamp function is supported
	strcpy(extensionstring, (char*)glGetString(GL_EXTENSIONS));

	currentextension = strtok(extensionstring, " ");

	while(currentextension)
	{
		if(!strcmp(currentextension, "GL_EXT_texture_edge_clamp"))
		{
			has_EXT_texture_edge_clamp = 1;
			break;
		}

		currentextension = strtok(0, " ");
	}

	//If the extension was not found, exit now
	if(!has_EXT_texture_edge_clamp)
	{
		printf("Could not initialize the EXT_texture_edge_clamp extension\n");
		return 0;
	}

	//Check to see if the EXT_blend_subtract function is supported
	strcpy(extensionstring, (char*)glGetString(GL_EXTENSIONS));

	currentextension = strtok(extensionstring, " ");

	while(currentextension)
	{
		if(!strcmp(currentextension, "GL_EXT_blend_subtract"))
		{
			has_EXT_blend_subtract = 1;
			break;
		}

		currentextension = strtok(0, " ");
	}

	//If the extension was not found, exit now
	if(!has_EXT_blend_subtract)
	{
		printf("Could not initialize the EXT_blend_subtract extension\n");
		return 0;
	}

	//Attempt to get the address of the glBlendEquationEXT function
	glBlendEquationEXT = (PFNGLBLENDEQUATIONEXTPROC) wglGetProcAddress("glBlendEquationEXT");

	//If the address returned is null, this extension is not
	//supported, so exit now
	if(!glBlendEquationEXT)
	{
		printf("Could not initialize the EXT_blend_subtract extension\n");
		return 0;
	}

	//Set shade model
	glShadeModel(GL_SMOOTH);
	//Enable depth testing
	glEnable(GL_DEPTH_TEST);

	//Set up lighting
	glLightfv(GL_LIGHT0, GL_AMBIENT, lightambient);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, lightdiffuse);
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos);
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 0);

	//Set up texgen
	glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glEnable(GL_TEXTURE_GEN_S);
	glTexGenfv(GL_S, GL_EYE_PLANE, eyeplane);
	
	//Create a texture to hold alpha data for the fog
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glGenTextures(1, &alphatexture);
	glBindTexture(GL_TEXTURE_1D, alphatexture);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE_EXT);
	glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, 
					GL_NEAREST);
	glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, 
					GL_NEAREST);
	glTexImage1D(GL_TEXTURE_1D, 0, GL_ALPHA, 256, 0, GL_ALPHA, GL_UNSIGNED_BYTE, alphadata);
	glEnable(GL_TEXTURE_1D);

	return 1;
}

void display(void)
{
	int i, j;
	int spheredraworder[4];
	int tempint;
	float tempfloat;
	float spheredistances[4];
	float spheredistance;
	float modelviewmatrix[16];

	//Set the camera position
	camerapos[0] = sin(rotation) * sin(elevation) * cameradistance;
	camerapos[1] = cos(elevation) * cameradistance;
	camerapos[2] = cos(rotation) * sin(elevation) * cameradistance;

	//Begin drawing the scene
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glLoadIdentity();
	gluLookAt(camerapos[0], camerapos[1], camerapos[2],
				0.0f, 0.0f, 0.0f,
				0.0f, 1.0f, 0.0f);

	glGetFloatv(GL_MODELVIEW_MATRIX, modelviewmatrix);

	//Draw the teapot and plane
	glEnable(GL_LIGHTING);
	glDisable(GL_CULL_FACE);
	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, teapotmaterial);
	glutSolidTeapot(1.0f);
	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, groundmaterial);
	glBegin(GL_QUADS);
	glNormal3f(0.0f, 1.0f, 0.0f);
	glVertex3f(-5.0f, -1.0f, 5.0f);
	glVertex3f(5.0f, -1.0f, 5.0f);
	glVertex3f(5.0f, -1.0f, -5.0f);
	glVertex3f(-5.0f, -1.0f, -5.0f);
	glEnd();
	glEnable(GL_CULL_FACE);
	glDisable(GL_LIGHTING);

	//Figure out which order to draw the fog volumes in
	for(i = 0; i < 4; i++)
	{
		spheredistance = sphere[i][0] * modelviewmatrix[2] +
							sphere[i][1] * modelviewmatrix[6] +
							sphere[i][2] * modelviewmatrix[10] +
							modelviewmatrix[14];
		spheredistance += sphere[i][3];
		spheredistance = -spheredistance;

		spheredistances[i] = spheredistance;
		spheredraworder[i] = i;
	}
	for(i = 0; i < 4; i++)
	{
		for(j = i + 1; j < 4; j++)
		{
			if(spheredistances[j] > spheredistances[j - 1])
			{
				tempfloat = spheredistances[j];
				spheredistances[j] = spheredistances[j - 1];
				spheredistances[j - 1] = tempfloat;

				tempint = spheredraworder[j];
				spheredraworder[j] = spheredraworder[j - 1];
				spheredraworder[j - 1] = tempint;
			}
		}
	}

	//Calculate fog contribution from fog volumes
	for(i = 0; i < 4; i++)
	{
		if(fogvisible[spheredraworder[i]])
		{
			//Clear the depth and stencil buffers
			glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

			//Set up the correct fog texgen
			glLoadIdentity();
			eyeplane[2] = -1.0f / (2.0f * sphere[spheredraworder[i]][3]);
			eyeplane[3] = spheredistances[i];
			eyeplane[3] *= eyeplane[2];

			glTexGenfv(GL_S, GL_EYE_PLANE, eyeplane);

			//Set OpenGL to only draw into alpha
			glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
			glColorMask(0, 0, 0, 1);

			glLoadIdentity();
			gluLookAt(camerapos[0], camerapos[1], camerapos[2],
				0.0f, 0.0f, 0.0f,
				0.0f, 1.0f, 0.0f);

			//Draw the teapot and plane into the alpha channel
			//to record the distance of each pixel from the camera
			glDisable(GL_LIGHTING);
			glDisable(GL_CULL_FACE);
			glutSolidTeapot(1.0f);
			glBegin(GL_QUADS);
			glVertex3f(-5.0f, -1.0f, 5.0f);
			glVertex3f(5.0f, -1.0f, 5.0f);
			glVertex3f(5.0f, -1.0f, -5.0f);
			glVertex3f(-5.0f, -1.0f, -5.0f);
			glEnd();

			//Set fog volume position
			glTranslatef(sphere[spheredraworder[i]][0], sphere[spheredraworder[i]][1], sphere[spheredraworder[i]][2]);

			//Back of fog volume
			glEnable(GL_CULL_FACE);
			glCullFace(GL_FRONT);
			glutSolidSphere(sphere[spheredraworder[i]][3], 32, 32);

			//Front of fog volume
			glEnable(GL_BLEND);
			glBlendFunc(GL_ONE, GL_ONE);
			glBlendEquationEXT(GL_FUNC_REVERSE_SUBTRACT_EXT);
			glCullFace(GL_BACK);
			glEnable(GL_STENCIL_TEST);
			glStencilFunc(GL_ALWAYS, 1, 0xFF);
			glStencilOp(GL_ZERO, GL_ZERO, GL_REPLACE);
			glutSolidSphere(sphere[spheredraworder[i]][3], 32, 32);

			glBlendEquationEXT(GL_FUNC_ADD_EXT);
			glDisable(GL_BLEND);
			glDisable(GL_STENCIL_TEST);
			glColorMask(1, 1, 1, 1);


			//Blend in fog
			glColor3f(fogcolor[spheredraworder[i]][0], fogcolor[spheredraworder[i]][1], fogcolor[spheredraworder[i]][2]);
			glLoadIdentity();
			glDisable(GL_CULL_FACE);
			glDisable(GL_TEXTURE_1D);
			glEnable(GL_BLEND);
			if(fogtype[spheredraworder[i]] == 0)
				glBlendFunc(GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA);
			else
				glBlendFunc(GL_DST_ALPHA, GL_ONE);
			glEnable(GL_STENCIL_TEST);
			glStencilFunc(GL_EQUAL, 1, 0xFF);
			glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
			glBegin(GL_QUADS);
			glVertex3f(-5.0f, -5.0f, -1.0f);
			glVertex3f(5.0f, -5.0f, -1.0f);
			glVertex3f(5.0f, 5.0f, -1.0f);
			glVertex3f(-5.0f, 5.0f, -1.0f);
			glEnd();
			glDisable(GL_STENCIL_TEST);
			glDisable(GL_BLEND);
			glEnable(GL_TEXTURE_1D);
			glEnable(GL_CULL_FACE);
		}
	}

	glutSwapBuffers();
}

void reshape(int w, int h)
{
	glViewport(0, 0, (GLsizei) w, (GLsizei) h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 0.1, 100.0);
	glMatrixMode(GL_MODELVIEW);
}

void mousemove(int x, int y)
{
	double modelviewmatrix[16], projectionmatrix[16];
	double projectedpoint[3];
	int viewport[4];
	float u;

	//Move camera
	if(controlmode == 0)
	{
		rotation += (x - lastx) / 100.0f;
		elevation += (y - lasty) / 100.0f;

		if(elevation < 0.1f)
			elevation = 0.1f;
		if(elevation > 1.5f)
			elevation = 1.5f;

		lastx = x;
		lasty = y;
	}
	//Change sphere position
	else if(controlmode == 1)
	{
		glLoadIdentity();
		gluLookAt(camerapos[0], camerapos[1], camerapos[2],
				0.0f, -0.5f, 0.0f,
				0.0f, 1.0f, 0.0f);
		glGetDoublev(GL_MODELVIEW_MATRIX, modelviewmatrix);
		glGetDoublev(GL_PROJECTION_MATRIX, projectionmatrix);
		glGetIntegerv(GL_VIEWPORT, viewport);

		gluUnProject(x, (viewport[3] - y), 1.0, modelviewmatrix, projectionmatrix, viewport,
				&(projectedpoint[0]), &(projectedpoint[1]), &(projectedpoint[2]));

		u = camerapos[1] / (camerapos[1] - projectedpoint[1]);

		sphere[currentfogvolume][0] = camerapos[0] + u * (projectedpoint[0] - camerapos[0]);
		sphere[currentfogvolume][2] = camerapos[2] + u * (projectedpoint[2] - camerapos[2]);
	}
}

void mousedown(int button, int state, int x, int y)
{
	lastx = x;
	lasty = y;
}

void keyboard (unsigned char key, int x, int y)
{
	int i;
	switch (key)
	{
		case 27:
			exit(0);
			break;
		case 'z':
			cameradistance += 0.5f;
			break;
		case 'x':
			cameradistance -= 0.5f;
			if(cameradistance < 4.0f)
				cameradistance = 4.0f;
			break;
		case 'a':
			sphere[currentfogvolume][3] += 0.2f;
			break;
		case 's':
			sphere[currentfogvolume][3] -= 0.2f;
			if(sphere[currentfogvolume][3] < 0.2f)
				sphere[currentfogvolume][3] = 0.2f;
			break;
		case 'f':
			fogtype[currentfogvolume] = -fogtype[currentfogvolume] + 1;
			break;
		case 'v':
			for(i = 0; i < 4; i++)
			{
				currentfogvolume++;
				currentfogvolume %= 4;
				if(fogvisible[currentfogvolume])
					break;
			}
			break;
		case 't':
			fogcolor[currentfogvolume][0] += 0.1f;
			if(fogcolor[currentfogvolume][0] > 1.0f)
				fogcolor[currentfogvolume][0] = 1.0f;
			break;
		case 'g':
			fogcolor[currentfogvolume][0] -= 0.1f;
			if(fogcolor[currentfogvolume][0] < 0.0f)
				fogcolor[currentfogvolume][0] = 0.0f;
			break;
		case 'y':
			fogcolor[currentfogvolume][1] += 0.1f;
			if(fogcolor[currentfogvolume][1] > 1.0f)
				fogcolor[currentfogvolume][1] = 1.0f;
			break;
		case 'h':
			fogcolor[currentfogvolume][1] -= 0.1f;
			if(fogcolor[currentfogvolume][1] < 0.0f)
				fogcolor[currentfogvolume][1] = 0.0f;
			break;
		case 'u':
			fogcolor[currentfogvolume][2] += 0.1f;
			if(fogcolor[currentfogvolume][2] > 1.0f)
				fogcolor[currentfogvolume][2] = 1.0f;
			break;
		case 'j':
			fogcolor[currentfogvolume][2] -= 0.1f;
			if(fogcolor[currentfogvolume][2] < 0.0f)
				fogcolor[currentfogvolume][2] = 0.0f;
			break;
		case 'd':
			fogvisible[currentfogvolume] = 0;
			for(i = 0; i < 4; i++)
			{
				currentfogvolume++;
				currentfogvolume %= 4;
				if(fogvisible[currentfogvolume])
					break;
			}
			fogvisible[currentfogvolume] = 1;
			break;
		case 'n':
			for(i = 0; i < 4; i++)
			{
				if(fogvisible[i] == 0)
				{
					currentfogvolume = i;
					fogvisible[currentfogvolume] = 1;
					fogcolor[currentfogvolume][0] = 1.0f;
					fogcolor[currentfogvolume][1] = 1.0f;
					fogcolor[currentfogvolume][1] = 1.0f;
					sphere[currentfogvolume][0] = 0.0f;
					sphere[currentfogvolume][1] = 0.0f;
					sphere[currentfogvolume][2] = 0.0f;
					sphere[currentfogvolume][3] = 2.0f;
					fogtype[currentfogvolume] = 0;
					break;
				}
			}
			break;
		default:
			break;
	}
}

int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
	glutInitWindowSize(800, 600);
	glutInitWindowPosition(100, 100);
	glutCreateWindow(argv[0]);
	
	if(!init())
		return 0;

	//Setup menu
	glutCreateMenu(menu);
	glutAddMenuEntry("Mouse changes view direction", 0);
	glutAddMenuEntry("Mouse changes sphere position", 1);
	glutAttachMenu(GLUT_RIGHT_BUTTON);

	//Set up control functions
	glutIdleFunc(idle);
	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutKeyboardFunc(keyboard);
	glutMotionFunc(mousemove);
	glutMouseFunc(mousedown);
	glutMainLoop();
	return 0; 
}
