/*
 * (C) Copyright Chris B. Kaynor 2006
 * All Rights Reserved.
 *
 * CONTACT: chris@kaynor.net
*/

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <assert.h>
#include <ctime> 
#include <string.h>

#ifdef __linux__
	#include <GL/glut.h>
	#include <GL/glext.h>
	#include <png.h>
	#include <pngconf.h>
	#include <zlib.h>
	#include <zconf.h>
#else
	#include "glut.h"
	#include "glext.h"
	#include "png.h"
	#include "pngconf.h"
	#include "zlib.h"
	#include "zconf.h"
#endif

//#include "load_image.h"

#include "vector.h"
#include "data.h"

static PFNGLACTIVETEXTUREARBPROC active_texture = NULL;

#define MaxRotationSpeed 25
#define MaxMovementSpeed 15
#define WorldSize 200.0
#define FogDensity 0.1
//#define FULL_SCREEN

#define GetRandom(MiniumValue, MaximumValue) (((MaximumValue - MiniumValue + 1) * (((double) rand()) / (((double) RAND_MAX) + 1)) + MiniumValue))

#define PI 3.1415926535
#define Degrees2Radians(Degrees) (Degrees * (PI / 180))
#define Radians2Degrees(Radians) (Radians * (180 / PI))

cbk_object_data_tank Tanks[100];
long Number_Tanks;

GLuint MaxTextureID = 0;

GLfloat Color_Fog[3] = {1, 1, 1};

GLfloat MovementSpeed = 0;
GLfloat RotationSpeed = 0;

/*
void LoadTexture(const char *TextureFile, int TextureID) {
	glBindTexture(GL_TEXTURE_2D, TextureID);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

	LoadImage(GL_TEXTURE_2D, 0, GL_RGB, TextureFile);
}
*/

/*
  Returns the time of the first collision between the two objects passed in, or -2 if no collision.
  Return will always be from 0 - 1, inclusive, or -2 if no collision.
*/
GLfloat DetectCollision(cbk_object_data l_Object1, cbk_object_data l_Object2, GLfloat time_slice) {
	if (time_slice == 0) {return -2;}

	cbk_vector q(l_Object1.get_position() - l_Object2.get_position()); //Combined Position Vector
	cbk_vector r((l_Object1.get_velocity() * time_slice) - (l_Object2.get_velocity() * time_slice)); //Combined Velocity Vector

	const GLfloat d = (l_Object1.GetCollsionSize() + l_Object2.GetCollsionSize()); //combined radius of collision spheres

	const GLfloat q_dot_r = (q * r);
	const GLfloat r_dot_r = (~r);
	const GLfloat q_dot_q = (~q);
	const GLfloat d_dot_d = (d * d);
	const GLfloat two_qr = (2 * q_dot_r);
	const GLfloat two_qr_squared = (two_qr * two_qr);
	const GLfloat D = (two_qr_squared - (4 * r_dot_r * (q_dot_q - d_dot_d)));

	if (D <= 0) {return -2;}

	const GLfloat sqrt_D = sqrt(D);
	const GLfloat t1 = (-two_qr + sqrt_D) / (2 * r_dot_r);
	const GLfloat t2 = (-two_qr - sqrt_D) / (2 * r_dot_r);
	GLfloat t;

    if ((t1 > 0) && (t1 < t2)) {
		t = t1;
    } else {
		t = t2;
    }

	return ((t > 1.0) || (t < 0.0)) ? -2 : t;
}

void SetupLevel(long LevelID) {
	GLfloat X_Pos, Y_Pos, Z_Pos;

	Number_Tanks = (LevelID * 2) + 1;

	Tanks[0].move(WorldSize / 2, 0, WorldSize / 2);
	Tanks[0].SetCollsionSize(1);
	Tanks[0].change_velocity(0, 0, 0);
	Tanks[0].rotate(0, 0, 0);

	for (long x = 1; x <= Number_Tanks; x++) {
		Tanks[x].move(GetRandom(0, WorldSize), 0, GetRandom(0, WorldSize));
		Tanks[x].SetCollsionSize(1);
		Tanks[x].change_velocity(0, 0, 0);
		Tanks[x].rotate(GetRandom(0, 360), 0, 0);
	}
}


static void init_checkers( void )
{
#define CUBE_TEX_SIZE 512
   GLubyte image[CUBE_TEX_SIZE][CUBE_TEX_SIZE][3];

   static const GLenum targets[6] = {
      GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB,
      GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB,
      GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB,
      GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB,
      GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB,
      GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB
   };

   GLint i, j, f;

   glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

   /* make colored checkerboard cube faces */
   for (f = 0; f < 6; f++) {
      for (i = 0; i < CUBE_TEX_SIZE; i++) {
         for (j = 0; j < CUBE_TEX_SIZE; j++) {
            if ((i/4 + j/4) & 1) {
               image[i][j][0] = 0;
               image[i][j][1] = 255;
               image[i][j][2] = 0;
            }
            else {
               image[i][j][0] = 255;
               image[i][j][1] = 0;
               image[i][j][2] = 255;
            }
         }
      }

      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, CUBE_TEX_SIZE, CUBE_TEX_SIZE, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
   }
}

void init(void)
{
	double version = 0;

	const char *const ver_string = (const char *) glGetString(GL_VERSION);

	if (ver_string != NULL) {
		version = strtod(ver_string, NULL);
	} else {
		printf("An error occured while trying to determine if the current version of OpenGL.\n");
		//exit(1);
	}

    glClearColor(0.2, 0.2, 0.2, 0.0);
    glShadeModel(GL_SMOOTH);

	glFogi(GL_FOG_MODE, GL_EXP2);
	glFogfv(GL_FOG_COLOR, Color_Fog);
	glFogf(GL_FOG_DENSITY, FogDensity);
	glHint(GL_FOG_HINT, GL_DONT_CARE);

	init_checkers();
	glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glEnable(GL_TEXTURE_CUBE_MAP_ARB);

	SetupLevel(5);
}

void DrawGround(void)
{
	const GLfloat Eye_Plane[] = {0, 0, 0};
	const GLfloat fExtent = (WorldSize * 1.5f);
	const GLfloat fStep = 2.0f;
	const GLfloat y = 0.0f;
	GLfloat iStrip, iRun;

	glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
	glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
	glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);

	glTexGenfv(GL_S, GL_EYE_PLANE, Eye_Plane);

	glEnable(GL_TEXTURE_GEN_S);
	glEnable(GL_TEXTURE_GEN_T);

	for(iStrip = -fExtent; iStrip <= fExtent; iStrip += fStep)
	{
		glBegin(GL_TRIANGLE_STRIP);
		glNormal3f(0.0f, 1.0f, 0.0f);

		for(iRun = fExtent; iRun >= -fExtent; iRun -= fStep)
		{
			glVertex3f(iStrip, y, iRun);
			glVertex3f(iStrip + fStep, y, iRun);
		}
		glEnd();
	}

	glDisable(GL_TEXTURE_GEN_S);
	glDisable(GL_TEXTURE_GEN_T);
}

void DrawSky(void) {
	glPushMatrix();

    glDisable(GL_DEPTH_TEST);
	glDisable(GL_FOG);

	glColor3f(0.0, 1.0, 1.0);

	glLineWidth(10);
	glLoadIdentity();
	glRotatef(-Tanks[0].get_rotation().GetValue(1), 0, 1, 0);
	glRotatef(-Tanks[0].get_rotation().GetValue(2), 1, 0, 0);
	glRotatef(-Tanks[0].get_rotation().GetValue(3), 0, 0, 1);
	glRotatef(90, 1, 0, 0);
	glutWireSphere(20, 30, 20);
	
	glEnable(GL_FOG);
    glEnable(GL_DEPTH_TEST);

	glPopMatrix();
}

/**
 * Display callback for GLUT.
 */
void display(void)
{
	cbk_vector PlayerPosition = Tanks[0].get_position();

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//Camera Setup
    glLoadIdentity();
    gluLookAt( 0.0, 1.0, -0.0,    // eye position
	           0.0, 1.0,  5.0,    // point the eye is looking at
	           0.0, 1.0,  0.0);   // "up" direction
		//Move and Rotate Camera
	glRotatef(-Tanks[0].get_rotation().GetValue(1), 0, 1, 0);
	glTranslatef(-PlayerPosition.GetValue(1), -PlayerPosition.GetValue(2), -PlayerPosition.GetValue(3));

	//Draw Enviroment
	DrawSky();
	DrawGround();

	//Draw Tanks
	for (long x = 0; x <= Number_Tanks; x++) {
		if (x == 0) {
			glColor3f(0.0f, 0.0f, 1.0f);
		} else {
			glColor3f(1.0f, 0.0f, 0.0f);
		}

		Tanks[x].draw();
	}

	//Display
	glutSwapBuffers();
}


void reshape(int width, int height)
{
    glViewport(0, 0, (GLsizei) width, (GLsizei) height);
	glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
	glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 20);
    glMatrixMode(GL_MODELVIEW);
}


void keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 27: // 27 is the ASCII code for escape.
		exit(0);
		break;
	case 'a':
		if (RotationSpeed > 0) {
			RotationSpeed *= 1.3;
		} else if (RotationSpeed < 0) {
			RotationSpeed *= 0.7;
		} else {
			RotationSpeed = 1;
		}

		if (abs(RotationSpeed) < 0.2) {RotationSpeed = 0;}
		if (RotationSpeed > MaxRotationSpeed) {RotationSpeed = MaxRotationSpeed;}
		if (RotationSpeed < -MaxRotationSpeed) {RotationSpeed = -MaxRotationSpeed;}

		break;
	case 'd':
		if (RotationSpeed < 0) {
			RotationSpeed *= 1.3;
		} else if (RotationSpeed > 0) {
			RotationSpeed *= 0.7;
		} else {
			RotationSpeed = -1;
		}

		if (abs(RotationSpeed) < 0.2) {RotationSpeed = 0;}
		if (RotationSpeed > MaxRotationSpeed) {RotationSpeed = MaxRotationSpeed;}
		if (RotationSpeed < -MaxRotationSpeed) {RotationSpeed = -MaxRotationSpeed;}

		break;
	case 'w':
		if (MovementSpeed > 0) {
			MovementSpeed *= 1.2;
		} else if (MovementSpeed < 0) {
			MovementSpeed *= 0.8;
		} else {
			MovementSpeed = 0.2;
		}

		if (abs(MovementSpeed) < 0.2) {MovementSpeed = 0;}
		if (MovementSpeed > MaxMovementSpeed) {MovementSpeed = MaxMovementSpeed;}
		if (MovementSpeed < -MaxMovementSpeed) {MovementSpeed = -MaxMovementSpeed;}

		break;
	case 's':
		if (MovementSpeed < 0) {
			MovementSpeed *= 1.2;
		} else if (MovementSpeed > 0) {
			MovementSpeed *= 0.8;
		} else {
			MovementSpeed = -0.2;
		}

		if (abs(MovementSpeed) < 0.2) {MovementSpeed = 0;}
		if (MovementSpeed > MaxMovementSpeed) {MovementSpeed = MaxMovementSpeed;}
		if (MovementSpeed < -MaxMovementSpeed) {MovementSpeed = -MaxMovementSpeed;}

		break;
	}
}

void idle(void)
{
	cbk_vector TankPosition = Tanks[0].get_position();
	cbk_vector TankRotation = Tanks[0].get_rotation();
	
	GLfloat XRotate = TankRotation.GetValue(1);

	//Determine time sience last call
	static GLfloat t0 = -1.0;
	const GLfloat t = glutGet(GLUT_ELAPSED_TIME) / 1000.0;
	const GLfloat dt = (t0 < 0.0) ? 0.00001 : t - t0;

	//Misc. Varibles
	GLfloat TimeSlice;
	bool Collide;

	//Update last call time
	t0 = t;

	//Rotate Player
	XRotate += RotationSpeed * dt;
	RotationSpeed *= 0.95;
	if (abs(RotationSpeed) < 0.1) {RotationSpeed = 0;}
	if (RotationSpeed > MaxRotationSpeed) {RotationSpeed = MaxRotationSpeed;}
	if (RotationSpeed < -MaxRotationSpeed) {RotationSpeed = -MaxRotationSpeed;}
	if (XRotate > 360) {XRotate -= 360;}
	if (XRotate <= 0) {XRotate += 360;}
	TankRotation.SetValue(1, XRotate);
	Tanks[0].rotate(TankRotation);

	//Move Player
	MovementSpeed *= 0.999;
	if (abs(MovementSpeed) < 0.1) {MovementSpeed = 0;}
	if (MovementSpeed > MaxRotationSpeed) {MovementSpeed = MaxMovementSpeed;}
	if (MovementSpeed < -MaxRotationSpeed) {MovementSpeed = -MaxMovementSpeed;}
	TankPosition.SetValue(1, TankPosition.GetValue(1) + (sin(Degrees2Radians(TankRotation.GetValue(1))) * (MovementSpeed * dt)));
	TankPosition.SetValue(3, TankPosition.GetValue(3) + (cos(Degrees2Radians(TankRotation.GetValue(1))) * (MovementSpeed * dt)));
	if (XRotate > 360) {XRotate -= 360;}
	if (XRotate <= 0) {XRotate += 360;}

	if (TankPosition.GetValue(1) > WorldSize) {TankPosition.SetValue(1, TankPosition.GetValue(1) - WorldSize);}
	if (TankPosition.GetValue(1) < 0)         {TankPosition.SetValue(1, TankPosition.GetValue(1) + WorldSize);}
	if (TankPosition.GetValue(3) > WorldSize) {TankPosition.SetValue(3, TankPosition.GetValue(3) - WorldSize);}
	if (TankPosition.GetValue(3) < 0)         {TankPosition.SetValue(3, TankPosition.GetValue(3) + WorldSize);}

	Tanks[0].move(TankPosition);

	//Loop though all tanks
	for (long x = 0; x <= Number_Tanks; x++) {
		for (long y = 0; y <= Number_Tanks; y++) {
			//Find time of first collision
			GLfloat dc_time = DetectCollision(Tanks[x], Tanks[y], dt);

			//Determine if collision is "real", and get the time to update for
			if ((dc_time < 0) || (dc_time > 1)) {
				TimeSlice = dt;
				Collide = false;
			} else {
				TimeSlice = dc_time * dt;
				Collide = true;
			}

			//Move Tanks
			Tanks[x].update(TimeSlice);
			Tanks[y].update(TimeSlice);
			if (Collide) {
				Tanks[x].change_velocity(0, 0, 0);
				Tanks[y].change_velocity(0, 0, 0);
			}
		}
	}

	glutPostRedisplay();
}

int main(int argc, char ** argv)
{	
	// Initialize GLUT.
	glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGB);

	// Setup Window
	#ifdef FULL_SCREEN
		glutGameModeString("width=800;height=600;bpp=24;hertz=60;");
		if (glutGameModeGet(GLUT_GAME_MODE_POSSIBLE)) {
			//GameMode advailibe, use it
			printf("The specified game mode is available. Using it.\n");

			glutEnterGameMode();

			printf("Using Game Mode: %ix%ix%i@%i\n", glutGameModeGet(GLUT_GAME_MODE_WIDTH), glutGameModeGet(GLUT_GAME_MODE_HEIGHT), glutGameModeGet(GLUT_GAME_MODE_PIXEL_DEPTH), glutGameModeGet(GLUT_GAME_MODE_REFRESH_RATE));
		} else {
			//GameMode not advailibe, use a full-screen window
			printf("The specified game mode is unavailable. Defaulting to standard window.\n");

			glutInitWindowSize(800, 600);
			glutCreateWindow("Combat Area");
			glutFullScreen();
		}
	#else
		//GameMode not advailibe, use a full-screen window
		printf("Full-screen mode is disabled in code.\n");

		glutInitWindowSize(800, 600);
		glutCreateWindow("Combat Area");
	#endif

    // Perform local program initialization.
    init();
	
    // Register our functions as GLUT callbacks.
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

    // Let 'er rip!
    glutMainLoop();

	// Make complier happy.
    return 0;
}
