#pragma comment(lib, "SDL.lib")
#pragma comment(lib, "SDLmain.lib")
#pragma comment(lib, "SDL_image.lib")

#include "SDL.h"
#include "SDL_image.h"

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


// FLESHY SPRITE.
// Stupid name made up for a, well, fleshy sprite.
// Normal sprites just sit there. Wow. A fleshy sprite can look different from different angles!
// A fleshy sprite can have a number of animations!

struct fleshySprite {
	SDL_Surface *textures[64*8];
	int frameOffset[64];
	int animationLength[64];
};

struct ray {
    double x;
    double y;
    double dX;
    double dY;
    double length;
    int adjX;
    int adjY;
	int texture_Num;
	bool hitDoor;
	double doorStatus;
	double doorX;
	double doorY;
	bool alreadyHitDoor;
};

struct objects {
	double x;			// X position
	double y;			// Y position
	double offX;			// Internal use only
	double offY;			//   |           |
	double yVecOffset;	//   V           V
	double xVecOffset;	// -----------------
	int type;			// "Type" (Soldier, ammo, weapon?)
	int model;			// Which model to draw 'em with
	int animation;		// Which animation loop to use
	int framePos;		// Frame # of current animation loop
	int angle;			// Angle they're looking at
	int timeLast;		// What time was it when they last did something?
	int targetAngle;	// What angle are they swinging towards?
};

struct changingDoor {
	int x;
	int y;
	double direction;
};

struct consoleVars {
	Uint8 *var;
	char *varName;
	char varFormat;
	char *varCaption;
};


// Global variables
	SDL_Surface *screen; // Pointer to screen surface

	int scrWidth = 320; // Screen dimensions
	int scrHeight = 240;

	int centreX;
	int centreY;

	const int maxDoorStates = 32;
	changingDoor changingDoors[maxDoorStates];

	const int fov = 60;
	const int mapSize = 64;

	int numAnglesInCircle;

	// LOOK UP TABLES: CREATED AT RUNTIME (WHEN SCREEN RES IS SET!)
	double *trigTable;	// numAnglesInCircle
	double *tanTable;	// numAnglesInCircle
	double *floorDistTable;
	double *pixelDistTable;
	double *depthBuffer;

	double playerX=32;
	double playerY=32;
	double playerS=0;
	double boundary=0.2;
	int playerA=0;
	double playerAd=0;

	int rFPS = -1;

	const int fontWidth = 7;
	const int fontHeight = 11;
	
	const int fontPlaceWidth = 8;
	const int fontPlaceHeight = 12;

	const int consoleNumLines = 10;
	const int consoleMaxStringLength = 255;

	bool hitError = false;

	int rObjects = 1;

	int numObjects;
	objects mapObjects[200];

	char consoleTextInput[consoleMaxStringLength]={0,};
    int consoleCursorPos = 0;

	char *consoleLines[consoleNumLines]={0,};
	bool showConsole = false;
	SDL_Surface *font;
	SDL_Rect fontPosition[256];

	SDL_Surface *numbers;

	int rFloors = 1;

	double doorStatus[mapSize][mapSize]={0,};
	int map[mapSize][mapSize]={0,};
	int mapFloors[mapSize][mapSize]={0,};
	int mapCeilings[mapSize][mapSize]={0,};

	long pixelLookup[128][128];

	// Image buffers:
	SDL_Surface *textures[64];	// wall textures
	
	int angleQuarter;
	int angleHalf;
	int angleThreeQuarter;

	int fullScreen = 0;

	SDL_Surface *healthHUD;

	SDL_Rect healthPos;

	const int MENUMODE_ROOTMENU = 0;
	const int MENUMODE_PLAYGAME = 1;
	const int MENUMODE_OPTIONS = 2;
SDL_Surface *loadAndMaskImage(char *fileName) {
	SDL_Surface *tempLoader = IMG_Load(fileName);
	if (tempLoader==NULL) return NULL;
	SDL_Surface *returnValue=SDL_CreateRGBSurface(SDL_HWSURFACE, tempLoader->w, tempLoader->h, 24, 0, 0, 0, 0);
	SDL_FillRect(returnValue,NULL,0xFF00FF);
	SDL_BlitSurface(tempLoader,NULL,returnValue,NULL);
	return returnValue;
}

fleshySprite *loadFleshySprite(char *fileName) {
	fleshySprite *returnSprite;
	returnSprite = new fleshySprite;
	printf("Loading fleshy sprite %s... ",fileName);
	FILE *spriteScript = fopen(fileName,"rb");
	if (spriteScript==NULL) {
		printf("Failed.\n");
		return NULL;
	} else {
		for (int i=0; i<64; ++i) {
			returnSprite->frameOffset[i] = (int)fgetc(spriteScript);
		}
		for (int i=0; i<64; ++i) {
			returnSprite->animationLength[i] = (int)fgetc(spriteScript);
		}

		int textureIndex = 0;
		int namePos = 0;
		char textureName[256] = {0,};
		char fullName[256] = {0,};


		while (true) {
			int a=(int)fgetc(spriteScript);
			if (a<0) break;
			textureName[namePos]=a;
			if (a==0) {
				if (namePos!=0) {
					printf("Loading texture #%i \"%s\"... ",textureIndex,textureName);
					strcpy(fullName,"resources/monsters/");
					strcat(fullName,textureName);
					returnSprite->textures[textureIndex]=loadAndMaskImage(fullName);
					if (returnSprite->textures[textureIndex]==NULL) {
						printf("Failed.\n");
					} else {
						printf("Done.\n");
					}
				}
				namePos=0;
				++textureIndex;
			} else {
				++namePos;
			}
		}

		fclose(spriteScript);
		printf("Done.\n");
		return returnSprite;
	}
	

}



Uint32 getpixel(SDL_Surface *surface, int x, int y) {
	if (x<0 || y<0) return 0;
    int bpp = surface->format->BytesPerPixel;
    /* Here p is the address to the pixel we want to retrieve */
    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        return *p;

    case 2:
        return *(Uint16 *)p;

    case 3:
		if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
            return p[0] << 16 | p[1] << 8 | p[2];
		} else {
            return p[0] | p[1] << 8 | p[2] << 16;
		}

    case 4:
        return *(Uint32 *)p;

    default:
        return 0;       /* shouldn't happen, but avoids warnings */
    }
}
void addLineToConsole(char *string) {
	//delete consoleLines[0];
	for (int i=0; i<consoleNumLines-1; ++i) {
		consoleLines[i]= consoleLines[i+1];
	}
	
	consoleLines[consoleNumLines-1]=string;
	for (int i=0; i<(strlen(string)/consoleMaxStringLength); ++i) {
		addLineToConsole("");
	}
}
void drawNumber(SDL_Surface *surface,int x, int y, int number, int places) {
	--places;
	int digits[30] = {0,};
	for (int n=places; n>=0; --n) {
		digits[places-n] = (int)(number/pow(10.0,n));
		if (digits[places-n]<0) digits[places-n]=0;
		number-=(int)(digits[places-n]*pow(10.0,n));
	}
	SDL_Rect fontPos;
	SDL_Rect scrPos;
	fontPos.h = 40;
	fontPos.w = 24;
	fontPos.y = 0;
	scrPos.w = 24;
	scrPos.h = 40;
	scrPos.y = y;

	
	for (int n=0; n<=places; ++n) {
		scrPos.x = n*24+x;
		fontPos.x = digits[n]*24;
		SDL_BlitSurface(numbers,&fontPos,surface,&scrPos);
	}

}
bool checkMapGrid(int x, int y) {
	if (map[x][y]>0) {
		return true;
	} else if (map[x][y]==0) {
		return false;
	} else if (map[x][y]<0) {
		for (int i=0; i<=maxDoorStates; ++i) {
			if (changingDoors[i].direction!=0  && changingDoors[i].x==x && changingDoors[i].y==y) {
				return true;
			} else {
				if (doorStatus[x][y]<0.2) {
					return false;
				} else {
					return true; 
				}
			}
		}
	}
}
bool checkPlayerPos(double x, double y) {
	double offX;
	double offY;
	/*for (int i=0; i<numObjects; ++i) {
		offX = mapObjects[i].x - playerX;
		offY = mapObjects[i].y - playerY;
		if (abs(offX) < boundary && abs(offY) < boundary) {
			return true;
		}
	}*/
	return checkMapGrid((int)(x-boundary),(int)(y-boundary)) || checkMapGrid((int)(x+boundary),(int)(y-boundary)) || checkMapGrid((int)(x-boundary),(int)(y+boundary)) || checkMapGrid((int)(x+boundary),(int)(y+boundary));
}
int normaliseAngle(int a) {
	if (a>0) {
		while (a>=numAnglesInCircle) {
			a-=numAnglesInCircle;
		}
		return a;
	} else {
		while (a<0) {
			a+=numAnglesInCircle;
		}
		return a;
	}
}
double radians(double a) {
    return a * 3.14159265358979 / angleHalf;
}
double qSin(int a) {
	return trigTable[normaliseAngle(a)];
}
double qCos(int a) {
	return qSin(a+angleQuarter);
}
double qTan(int a) {
	return tanTable[normaliseAngle(a)];
}
bool setupScreen() {
	Uint32 flags = SDL_HWSURFACE|SDL_DOUBLEBUF;
	if (fullScreen) flags = flags|SDL_FULLSCREEN;
	screen = SDL_SetVideoMode(scrWidth, scrHeight, 32, flags);


	if (screen==NULL) {
		return false;
	}

	// Init
	centreY = scrHeight>>1;
	centreX = scrWidth>>1;
	numAnglesInCircle = (int)(scrWidth*360)/fov;
	angleQuarter = numAnglesInCircle/4;
	angleHalf = numAnglesInCircle/2;
	angleThreeQuarter = 3*numAnglesInCircle/4;

	trigTable = new double[numAnglesInCircle];
	tanTable = new double[numAnglesInCircle];
	depthBuffer = new double[scrWidth];
	pixelDistTable = new double[scrHeight*scrWidth];
    

	// Create look-up tables
	printf("Creating look-up tables (%i)... ",numAnglesInCircle);
	for (int i=0; i<numAnglesInCircle; ++i) {
		trigTable[i] = sin(radians(i));
		tanTable[i] = tan(radians(i));
	}


	for (int a=0; a<scrWidth; ++a) {
		for (int d=0; d<scrHeight/2; ++d) {
			pixelDistTable[d+a*scrHeight]=((double)(centreY)/(double)(centreY-d))*(1/qCos((scrWidth>>1)-a));
		}
	}

	healthPos.x = scrWidth - healthHUD->w;
	healthPos.y = scrHeight - healthHUD->h;

	if (screen!=NULL) {
		SDL_FillRect(screen,NULL,0);
	}

	printf("Done.\n");

	return true;
}

void executeConsoleCommand() {
	char newChar[256] = {0,};
	char *sendToConsole = new char[consoleCursorPos+1];
	char *returnString = new char[256];
	char formatString[256] = {0,};

	strcpy(newChar, consoleTextInput);
	strcpy(sendToConsole, consoleTextInput);
	addLineToConsole(sendToConsole);


	int playerRA = (playerA*360)/numAnglesInCircle;
	int oldPA = playerRA;
	double oldPX = playerX;
	double oldPY = playerY;
	int oSW = scrWidth;
	int oSH = scrHeight;

	const int numConsoleVars = 8;

	consoleVars consoleVariables[] =
	{
		(Uint8*)&playerX, "p.x", 'f', "Player's x coord",
		(Uint8*)&playerY, "p.y", 'f', "Player's y coord",
		(Uint8*)&playerRA, "p.a", 'i', "Player's angle [degs]",
		(Uint8*)&rFloors, "r.floors", 'i', "Render floors/ceilings",
		(Uint8*)&rFPS, "r.fps", 'i', "Renderer's FPS count",
		(Uint8*)&rObjects, "r.objects", 'i', "Render objects",
		(Uint8*)&scrWidth, "s.w", 'i', "Screen's width",
		(Uint8*)&scrHeight, "s.h", 'i', "Screen's height"
	};

	// Parse request:
	
	strcpy (returnString, "Command not understood.");

	if (newChar[0]=='s' && newChar[1]=='e' && newChar[2]=='t' && newChar[3]==' ') {
		char lookUp[256]={0,};
		int p = 0;
		while (newChar[p+4]!=' ' && newChar[p+4]!=0) {
			lookUp[p]=newChar[p+4];
			++p;
		}
		strcpy(newChar,lookUp);
		p = 4;
		while (consoleTextInput[p]!=' ' && consoleTextInput[p]!=0) {
			++p;
		}
		if (consoleTextInput[p]!=0) {
			consoleTextInput[p]=0;
			char newVal[256] = {0,};
			strcpy(newVal, &consoleTextInput[p+1]);
			for (int i=0; i<numConsoleVars; ++i) {
				if (_stricmp(newChar, consoleVariables[i].varName)==0) {
					double f=0;
					int j=0;
					bool b=false;
					switch (consoleVariables[i].varFormat) {
						case 'f':
							sscanf(newVal, "%f", &f);
							*(double*)consoleVariables[i].var = f;
							break;
						case 'i':
							sscanf(newVal, "%i", &j);
							*(int*)consoleVariables[i].var = j;
							break;
					}
				}
			}
		}
	}

	if (playerX<1) playerX=1;
	if (playerY<1) playerY=1;
	if (playerX>=mapSize-1) playerX=mapSize-2;
	if (playerY>=mapSize-1) playerY=mapSize-2;
	if (checkPlayerPos((int)playerX, (int)playerY)) {
		playerX = oldPX;
		playerY = oldPY;
	}

	for (int i=0; i<numConsoleVars; ++i) {
		if (_stricmp(newChar, consoleVariables[i].varName)==0) {
			strcpy(formatString, consoleVariables[i].varName);
			strcat(formatString, " = %");
			char formatChar[2] = {consoleVariables[i].varFormat,0};
			strcat(formatString, formatChar);
			strcat(formatString, " (");
			strcat(formatString, consoleVariables[i].varCaption);
			strcat(formatString, ")");
			switch (consoleVariables[i].varFormat) {
				case 'f': sprintf(returnString,formatString,*(double*)consoleVariables[i].var); break;
				case 'i': sprintf(returnString,formatString,*(int*)consoleVariables[i].var); break;
				default: strcpy(returnString,"Could not convert format."); break;
			}
			break;
		}
	}


	if (returnString[0]!=0) {
		addLineToConsole(returnString);
	}

	consoleCursorPos=0;
	for (int i=0; i<=consoleMaxStringLength; ++i) {
		consoleTextInput[i] = 0;
	}

	if (playerRA!=oldPA) {
		playerA = normaliseAngle((playerRA*numAnglesInCircle)/360);
	}

	if (oSW!=scrWidth || oSH!=scrHeight) {
		setupScreen();
	}


}
void consolePutC(SDL_Surface *surface, int chr, int x, int y) {
	SDL_Rect dest;
	dest.h = fontHeight;
	dest.w = fontWidth;
	dest.x = x;
	dest.y = y;
    SDL_BlitSurface(font,&fontPosition[chr],screen,&dest);
}
SDL_Rect consolePutString(SDL_Surface *surface, char *string, int x, int y) {
	SDL_Rect cursorPos;
	cursorPos.x = x;
	cursorPos.y = y;
	if (string!=0) {
	while (*string!=0) {
		int chr = *string;
		consolePutC(surface, chr, cursorPos.x, cursorPos.y);
		cursorPos.x+=fontPlaceWidth;
		if (cursorPos.x>=scrWidth || *string=='\n') {
			cursorPos.x = 0;
			cursorPos.y += fontPlaceHeight;
		}
		++string;
	}
	}
	return cursorPos;
}

bool loadFont(char *fontfile) {
	font = IMG_Load(fontfile);
	
	if (font==NULL) {
		return false;
	} else {
		for (int i=0; i<256; ++i) {
			fontPosition[i].h = fontHeight;
			fontPosition[i].w = fontWidth;
			fontPosition[i].y = 0;
			if (i<=32 || i>126) {
				fontPosition[i].x = 0;
			} else {
				fontPosition[i].x = fontWidth*(i-32);
			}
		}
		return true;
	}
}
void putpixel(SDL_Surface *surface, int x, int y, Uint32 pixel) {
    int bpp = surface->format->BytesPerPixel;
    /* Here p is the address to the pixel we want to set */
    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        *p = pixel;
        break;

    case 2:
        *(Uint16 *)p = pixel;
        break;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
            p[0] = (pixel >> 16) & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = pixel & 0xff;
        } else {
            p[0] = pixel & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = (pixel >> 16) & 0xff;
        }
        break;

    case 4:
        *(Uint32 *)p = pixel;
        break;
    }
}
bool Slock(SDL_Surface *screen) {
	if (SDL_MUSTLOCK(screen)) {
		if (SDL_LockSurface(screen)==0) {
			return true;
		} else {
			return false;
		}
	} else {
		return true;
	}
}

void Sulock(SDL_Surface *screen) {
  if (SDL_MUSTLOCK(screen)) {
    SDL_UnlockSurface(screen);
  }
}
void drawTextureSlice(SDL_Surface *texture, SDL_Surface *dest, int column, int destX, long destH) {
	if (destH>0 && destH<scrHeight*50) {
		column = column % texture->w;
		int yTop = centreY-destH/2;
		int yBottom = yTop+destH;
		int rTop = yTop;
		int rBottom = yBottom;
		int textureSize = texture->h;
		if (rTop<0) rTop=0;
		if (rBottom>=scrHeight) rBottom=scrHeight-1;
		Uint16 destSkipBytes = dest->pitch;
		Uint8 *q = (Uint8*)dest->pixels+rTop*dest->pitch+destX*dest->format->BytesPerPixel;
		int textureY;
		
		int yI = (rTop-yTop)*textureSize;
		int lastTextureY=yI/destH;
		int textureSkipBytes = texture->pitch;
		Uint8 *p = (Uint8*)texture->pixels + column*texture->format->BytesPerPixel + texture->pitch*lastTextureY;
		long maxP = (long)texture->pixels+column*texture->format->BytesPerPixel+textureSkipBytes*textureSize;
		for (int y=rTop; y<=rBottom; ++y) {
			if ((*(Uint32*)p&0xFFFFFF)!=0xFF00FF) {
				if (Slock(screen)) {
					*(Uint32*)q = *(Uint32*)p;
					Sulock(screen);
				}
			}
			q+=destSkipBytes;	// Move down one
			textureY=yI/destH;
			if (textureY>=textureSize) break;
			for (int i=lastTextureY; i<textureY; ++i) {
				p+=textureSkipBytes;
			}
			lastTextureY=textureY;
			yI+=textureSize;
		}
	}
}


bool loadMap(char *filename) {
	printf("Loading map \"%s\"... ",filename);
	FILE *mapToLoad = fopen(filename,"r");
	if (mapToLoad==NULL) {
		printf("Failed.\n");
		return false;
	} else {
		int mapX = (int)fgetc(mapToLoad);
		int mapY = (int)fgetc(mapToLoad);
		printf("\nMap size: %ix%i\n",mapX,mapY);
		int rpX = (int)fgetc(mapToLoad);
		int rpY = (int)fgetc(mapToLoad);
		int rpA = (int)fgetc(mapToLoad);
		printf("Player coordinates: (%i,%i) (facing: %i)\n",rpX,rpY,rpA);
		for (int m=0; m<=2; ++m) {
			for (int y=0; y<mapY; ++y) {
				for (int x=0; x<mapX; ++x) {
					int getByte = (int)fgetc(mapToLoad);
					if (m==0) {
						if (getByte<64) {
							map[x][y]=getByte;
						} else {
							map[x][y]=-(getByte-64);
						}
					} else if (m==1) {
						mapFloors[x][y]=getByte;
					} else {
						mapCeilings[x][y]=getByte;
					}
					doorStatus[x][y]=1;
				}
			}
		}
		int textureIndex = 0;

		int namePos = 0;
		char textureName[256] = {0,};
		char fullName[256] = {0,};


		while (true) {
			int a=(int)fgetc(mapToLoad);
			if (a<0) break;
			textureName[namePos]=a;
			if (a==0) {
				
				if (namePos!=0) {
					printf("Loading texture #%i \"%s\"... ",textureIndex,textureName);
					strcpy(fullName,"resources/textures/");
					strcat(fullName,textureName);
					SDL_Surface *tempLoader = IMG_Load(fullName);
					textures[textureIndex]=SDL_CreateRGBSurface(SDL_HWSURFACE, tempLoader->w, tempLoader->h, 24, 0, 0, 0, 0);
					SDL_BlitSurface(tempLoader,NULL,textures[textureIndex],NULL);
					SDL_FreeSurface(tempLoader);

					if (textures[textureIndex]==NULL) {
						printf("Failed.\n");
					} else {
						printf("Done.\n");
					}
				}
				namePos=0;
				++textureIndex;
			} else {
				++namePos;
			}
		}

		fclose(mapToLoad);
		printf("Done.\n");
		return true;
	}
	
}
void quickSort (objects a[], int lo, int hi) {
	int i=lo, j=hi;
	objects h;
	double x=a[(lo+hi)/2].yVecOffset;
	do {
		while (a[i].yVecOffset>x) i++; 
		while (a[j].yVecOffset<x) j--;
		if (i<=j) {
			h=a[i]; a[i]=a[j]; a[j]=h;
			i++; j--;
		}
	} while (i<=j);

	if (lo<j) quickSort(a, lo, j);
	if (i<hi) quickSort(a, i, hi);
}
int main(int argc, char **argv) {

	for (int i=0; i<consoleNumLines; ++i) {
		addLineToConsole(" ");
	}

	printf("SDL RAYCASTER - BEN RYVES 2004\n");



	// Start up SDL
	printf("Starting up SDL... ");
	if (SDL_Init(SDL_INIT_VIDEO)<0)	{
		printf("Video initialization failed: %s\n",SDL_GetError());
		SDL_Quit();
		return 1;
	} else {
		printf("Done.\n");
	}

	for (int x=0; x<128; ++x) {
		for (int y=0; y<128; ++y) {
			pixelLookup[x][y] = y*384+x*3;
		}
	}

	SDL_WM_SetCaption("Raycaster",NULL);



	
	printf("Loading font for console... ");
	if (loadFont("resources/gui/small_text.gif")) {
		printf("Done.\n");
	} else {
		printf("Failed.\n");
		return 0;
	}

	// Load the map
	if (!loadMap("resources/maps/test_map.rcmap")) return 1;

	// Add some objects:

	
	numObjects = 20;

	fleshySprite *soldier = loadFleshySprite("resources/monsters/soldier.rcani");

	for (int i=0; i<numObjects; ++i) {
		mapObjects[i].x = (rand()&0xFFFF);
		mapObjects[i].y = (rand()&0xFFFF);
		mapObjects[i].x/=256*2;
		mapObjects[i].y/=256*2;
		mapObjects[i].model = 0;
		mapObjects[i].angle = 0;
		mapObjects[i].animation = 1;
		mapObjects[i].framePos = 0;
	}


	printf("Loading graphics... ");
	numbers = IMG_Load("resources/gui/numbers.gif");
	healthHUD = IMG_Load("resources/gui/health.gif");
	SDL_Surface *menuBack = IMG_Load("resources/gui/menu_back.gif");
	SDL_Surface *menuText = IMG_Load("resources/gui/menu_options.gif");
	SDL_Surface *menuTextGreyed = IMG_Load("resources/gui/menu_options_greyed.gif");
	if (healthHUD==NULL||numbers==NULL||menuBack==NULL||menuText==NULL||menuTextGreyed==NULL) {
		printf("Failed.\n");
		return 1;
	} else {
		printf("Done.\n");
	}
	
	// Load config file!

	FILE *config;
	config = fopen("resources/config.cfg","r");
	if (config==NULL) {
		printf("Error reading configuration file!\n");
	} else {
		scrWidth = _getw(config);
		scrHeight = _getw(config);
		fullScreen = _getw(config);
		rFloors = _getw(config);
		fclose(config);
		printf("Read configuration file.\n");
	}


	// Create screen surface
	printf("Creating screen surface... ");
	setupScreen();
	if (screen==NULL) {
		printf("Unable to set video mode: %s\n", SDL_GetError());
		SDL_Quit();
		return 1;
	} else {
		printf("Done.\n");
	}

	bool terminate = false;
	SDL_Event event;

	int fpsCount = 0;
	int lastFrameIndex = 0;

	int lastGameLoop = 0;

	bool lookingAtDoor;
	int lookingDoorX;
	int lookingDoorY;

	int menuMode=MENUMODE_ROOTMENU;
	int menuOption = 0;

	int lastAnimate = 0;

	while (!terminate) {

		// ================================================================================
		// MAIN MENU
		// ================================================================================

		while (menuMode==MENUMODE_ROOTMENU && !terminate) {
			while (SDL_PollEvent(&event)) {
				if (event.type==SDL_QUIT) {
					terminate=true;
				}
				if (event.type==SDL_KEYDOWN) {
					if (event.key.keysym.sym==SDLK_UP && menuOption>0) {
						--menuOption;
					} else if (event.key.keysym.sym==SDLK_DOWN && menuOption<5) {
						++menuOption;
					} else if (event.key.keysym.sym==SDLK_RETURN) {
						if (menuOption==0) {
							menuMode = MENUMODE_PLAYGAME;
						} if (menuOption==5) {
							terminate = true;
						} if (menuOption==3) {
							menuMode = MENUMODE_OPTIONS;
						}
					}
				}
			}

			SDL_Rect menuBackPlace;
			menuBackPlace.w = 320;
			menuBackPlace.h = 240;
			menuBackPlace.x = centreX-320/2;
			menuBackPlace.y = centreY-240/2;

			SDL_BlitSurface(menuBack, NULL, screen, &menuBackPlace);

			for (int i=0; i<6; ++i) {
				SDL_Rect menuOptionsPos;
				menuOptionsPos.w = 160;
				menuOptionsPos.h = 36;
				menuOptionsPos.x = centreX-160/2;
				menuOptionsPos.y = i*36+15;
				SDL_Rect menuOptionsGetPos = menuOptionsPos;
				menuOptionsGetPos.x = 0;
				menuOptionsPos.y+=centreY-240/2;
				if (i==menuOption) {
					SDL_BlitSurface(menuText, &menuOptionsGetPos, screen, &menuOptionsPos);
				} else {
					SDL_BlitSurface(menuTextGreyed, &menuOptionsGetPos, screen, &menuOptionsPos);
				}
			}

			SDL_Flip(screen);
		}

		// ================================================================================
		// OPTIONS MENU
		// ================================================================================

		if (menuMode==MENUMODE_OPTIONS && !terminate) {
			SDL_Rect **modes;
			modes=SDL_ListModes(NULL, SDL_HWSURFACE|SDL_FULLSCREEN);
			int curMode = 0;
			for(int i=0; modes[i]; ++i) {
				//printf("%ix%i\n",modes[i]->w,modes[i]->h);
				if (modes[i]->h==scrHeight && modes[i]->w==scrWidth) {
					curMode = i;
					break;
				}
			}
			int curSetting = 0;
			bool applySettings = false;
			int newFullscreen = fullScreen;
			int newRFloors = rFloors;
			while (menuMode==MENUMODE_OPTIONS && !terminate) {
				while (SDL_PollEvent(&event)) {
					if (event.type==SDL_QUIT) {
						terminate=true;
					}
					if (event.type==SDL_KEYDOWN) {
						if (event.key.keysym.sym==SDLK_ESCAPE) {
							menuMode = MENUMODE_ROOTMENU;
						} else if (event.key.keysym.sym==SDLK_UP && curSetting>0) {
							--curSetting;
						} else if (event.key.keysym.sym==SDLK_DOWN && curSetting<3) {
							++curSetting;
						} else if (event.key.keysym.sym==SDLK_RIGHT) {
							switch (curSetting) {
								case 0:
									if (curMode>0) --curMode;
									break;
								case 1:
									newFullscreen = 1;
									break;
								case 2:
									newRFloors = 1;
									break;
								case 3:
									applySettings = true;
									menuMode = MENUMODE_ROOTMENU;
									break;
							}
						} else if (event.key.keysym.sym==SDLK_LEFT) {
							switch (curSetting) {
								case 0:
									++curMode;
									if (!modes[curMode]) --curMode;
									if ((modes[curMode]->h)<240 || (modes[curMode]->w)<320) --curMode;
									break;
								case 1:
									newFullscreen = 0;
									break;
								case 2:
									newRFloors = 0;
									break;
							}
						}
					}
				}

				SDL_Rect menuBackPlace;
				menuBackPlace.w = 320;
				menuBackPlace.h = 240;
				menuBackPlace.x = centreX-320/2;
				menuBackPlace.y = centreY-240/2;

				SDL_BlitSurface(menuBack, NULL, screen, &menuBackPlace);

				SDL_Rect menuOptionsPos;
				menuOptionsPos.w = 160;
				menuOptionsPos.h = 35;
				menuOptionsPos.x = centreX-160/2;
				menuOptionsPos.y = 3*36+15;

				SDL_Rect menuOptionsGetPos = menuOptionsPos;
				menuOptionsGetPos.x = 0;
				menuOptionsPos.y=centreY-240/2+30;
				SDL_BlitSurface(menuText, &menuOptionsGetPos, screen, &menuOptionsPos);

				char dispMode[256];
				sprintf(dispMode,"Screen Resolution: %ix%i",modes[curMode]->w,modes[curMode]->h);
				consolePutC(screen,'>',centreX-130,centreY-50+curSetting*12);
				consolePutString(screen,dispMode,centreX-120,centreY-50);
				if (newFullscreen) {
					strcpy(dispMode,"Fullscreen: Yes");
				} else {
					strcpy(dispMode,"Fullscreen: No");
				}
				consolePutString(screen,dispMode,centreX-120,centreY-50+12);
				if (newRFloors) {
					strcpy(dispMode,"Draw Ceilings and Floors: Yes");
				} else {
					strcpy(dispMode,"Draw Ceilings and Floors: No");
				}
				consolePutString(screen,dispMode,centreX-120,centreY-50+24);
				consolePutString(screen,"Apply Settings (Press Right)",centreX-120,centreY-50+36);

				SDL_Flip(screen);
			}
			if (applySettings) {
				scrHeight = modes[curMode]->h;
				scrWidth = modes[curMode]->w;
				fullScreen = newFullscreen;
				rFloors = newRFloors;
				setupScreen();
				// At this point, save the settings to disk.
				FILE *config;
				config = fopen("resources/config.cfg","w");
				if (config==NULL) {
					printf("Error writing configuration file!\n");
				} else {
					_putw(scrWidth,config);
					_putw(scrHeight,config);
					_putw(fullScreen,config);
					_putw(rFloors,config);
					fclose(config);
					printf("Written configuration file to disk.\n");
				}

			}
		}

		// ================================================================================
		// GAME ("no menu")
		// ================================================================================

		if (menuMode==MENUMODE_PLAYGAME && !terminate) {
			while (menuMode==MENUMODE_PLAYGAME && !terminate) {
				while (SDL_PollEvent(&event)) {
					if (event.type==SDL_QUIT) {
						terminate=true;
					} else if (event.type==SDL_KEYDOWN) {
						if (event.key.keysym.sym==SDLK_LEFT) {
							playerAd = -angleQuarter;
						} else if (event.key.keysym.sym==SDLK_RIGHT) {
							playerAd = angleQuarter;
						} else if (event.key.keysym.sym==SDLK_UP) {
							playerS = -1; 
						} else if (event.key.keysym.sym==SDLK_DOWN) {
							playerS = 1;
						} else if (event.key.keysym.sym==SDLK_RETURN && event.key.keysym.mod&KMOD_ALT) {
							fullScreen=!fullScreen;
							setupScreen();
						} else if (event.key.keysym.sym==SDLK_ESCAPE) {
							menuMode=MENUMODE_ROOTMENU;
						}  else if (event.key.keysym.sym==SDLK_SPACE) {
							if (lookingAtDoor==true) {
								int doorPointer = 0;
								while (doorPointer<maxDoorStates && changingDoors[doorPointer].direction!=0 && changingDoors[doorPointer].x!=lookingDoorX && changingDoors[doorPointer].y!=lookingDoorY) {
									++doorPointer;
								}
								if (doorPointer!=maxDoorStates) {
									changingDoors[doorPointer].x = lookingDoorX;
									changingDoors[doorPointer].y = lookingDoorY;
									if (changingDoors[doorPointer].direction==0) {
										if (doorStatus[lookingDoorX][lookingDoorY]>0.5) {
											changingDoors[doorPointer].direction = -0.001;
										} else {
											changingDoors[doorPointer].direction = +0.001;
										}
									} else {
										printf("Door already moving!\n");
										changingDoors[doorPointer].direction = -changingDoors[doorPointer].direction;
									}
								} else {
									printf("ERROR: Could not add changing door state!\n");
								}
							}
						} else if (event.key.keysym.scancode==41) {
							showConsole=!showConsole;
							if (showConsole) {
								SDL_EnableUNICODE(1);
							} else {
								SDL_EnableUNICODE(0);
							}
						}
						// Handle console input
						if (showConsole) {
							int k = event.key.keysym.unicode;
							if (event.key.keysym.sym==SDLK_BACKSPACE) {
								--consoleCursorPos;
								if (consoleCursorPos<0) consoleCursorPos=0;
								consoleTextInput[consoleCursorPos] = 0;
							} else {
								if (k>=32 && k<=126) {
									consoleTextInput[consoleCursorPos] = k;
									++consoleCursorPos;
								} 
								if (consoleCursorPos>=consoleMaxStringLength) --consoleCursorPos;
							}
							if (event.key.keysym.sym==SDLK_RETURN) {
								executeConsoleCommand();
							}
						}
					} else if (event.type==SDL_KEYUP) {
						if (event.key.keysym.sym==SDLK_LEFT||event.key.keysym.sym==SDLK_RIGHT) playerAd = 0;
						if (event.key.keysym.sym==SDLK_UP||event.key.keysym.sym==SDLK_DOWN) playerS = 0;
					}

				}


				if (SDL_GetTicks() >= lastFrameIndex+1000) {
					printf("FPS: %i\n",fpsCount);
					rFPS = fpsCount;
					fpsCount = 0;
					lastFrameIndex = SDL_GetTicks();
				}

				if (SDL_GetTicks() >= lastAnimate+100) {
					for (int i=0; i<numObjects; ++i) {
						mapObjects[i].framePos = (mapObjects[i].framePos+1)%soldier->animationLength[mapObjects[i].animation];
					}
					lastAnimate = SDL_GetTicks();
				}

				// GAME CODE

				int time = (SDL_GetTicks() - lastGameLoop);
				lastGameLoop = SDL_GetTicks();

				for (int i=0; i<maxDoorStates; ++i) {
					if (changingDoors[i].direction!=0) {
						doorStatus[changingDoors[i].x][changingDoors[i].y]+=changingDoors[i].direction * time;
					}
					if (doorStatus[changingDoors[i].x][changingDoors[i].y]<0) {
						doorStatus[changingDoors[i].x][changingDoors[i].y]=0;
						changingDoors[i].direction = 0;
					} else if (doorStatus[changingDoors[i].x][changingDoors[i].y]>1) {
						doorStatus[changingDoors[i].x][changingDoors[i].y]=1;
						changingDoors[i].direction = 0;
					}
					
				}
				double testX;
				double testY;
				playerA-=(playerAd*time)/512;
				playerA = normaliseAngle(playerA);
				testX=playerX-(playerS*time*(qSin(playerA)/256));
				testY=playerY-(playerS*time*(qCos(playerA)/256));
				
				if (!checkPlayerPos(testX, playerY)) playerX = testX;
				if (!checkPlayerPos(playerX, testY)) playerY = testY;

				// Enemy AI. And it's more A than I, lemme tell you!

				for (int i=0; i<numObjects; ++i) {
					testX=mapObjects[i].x-(time*(qSin(mapObjects[i].angle)/512));
					testY=mapObjects[i].y-(time*(qCos(mapObjects[i].angle)/512));



					bool hitWall = checkPlayerPos(testX,testY);
					if (!checkPlayerPos(testX, mapObjects[i].y)) mapObjects[i].x = testX;
					if (!checkPlayerPos(mapObjects[i].x, testY)) mapObjects[i].y = testY;
					if (hitWall && (mapObjects[i].angle==mapObjects[i].targetAngle)) {
						mapObjects[i].targetAngle = normaliseAngle(mapObjects[i].angle + 1-(rand()%2)*angleHalf + 32/(32+rand()%32)*angleQuarter);
					}
					if (mapObjects[i].targetAngle!=mapObjects[i].angle) {
						int rA = mapObjects[i].angle + angleHalf;
						int rTA = mapObjects[i].targetAngle + angleHalf;

						if (abs(rA-rTA) < time) {
							mapObjects[i].angle = mapObjects[i].targetAngle;
						} else if (rA>rTA) {
                            mapObjects[i].angle = normaliseAngle(mapObjects[i].angle - time);
						} else {
							mapObjects[i].angle = normaliseAngle(mapObjects[i].angle + time);
						}                      						
					}
				}

				if (screen==NULL) {
					printf("Could not find device to draw to - quitting.\n");
					hitError = true;
					terminate = true;
				} else {

					// Variables used in raycasting
					ray horizontal;
					ray vertical;

					int s_Height = 0;
					double wall_Dist = 10;
					int wall_Slice;
					int texture_Num;
					int rayAngle;
					int rayAngleVert;

					lookingAtDoor = false;


					if (!rFloors) {
						SDL_FillRect(screen, NULL, 0);
					}

					Slock(screen);

					for (int r=0; r<scrWidth; ++r) {
						rayAngle = normaliseAngle((scrWidth>>1)-r+playerA);
						rayAngleVert = normaliseAngle(rayAngle-angleQuarter);
						// Create the horizontal ray details:
						horizontal.dX = qTan(rayAngle);
						horizontal.hitDoor = false;
						horizontal.alreadyHitDoor = false;
						horizontal.doorStatus = 0;
    					if (rayAngle>angleQuarter &&  rayAngle<angleThreeQuarter) {
							horizontal.y = (int)playerY;
							horizontal.adjY = 1;
							horizontal.dY = -1;
							horizontal.dX = -horizontal.dX;
						} else {
							horizontal.y = (int)playerY + 1;
							horizontal.adjY = 0;
							horizontal.dY = 1;
						}
    					horizontal.x = playerX + (horizontal.y - playerY) * qTan(rayAngle);

						// Create the vertical ray details:
						vertical.hitDoor = false;
						vertical.alreadyHitDoor = false;
						vertical.dY = qTan(rayAngleVert);
						vertical.doorStatus = 0;
						if (rayAngleVert>angleQuarter &&  rayAngleVert<angleThreeQuarter) {
							vertical.x = (int)playerX;
							vertical.adjX = 1;
							vertical.dX = -1;
						} else {
							vertical.x = (int)playerX + 1;
							vertical.adjX = 0;
							vertical.dX = 1;
							vertical.dY = -vertical.dY;
						}
						vertical.y = playerY - (vertical.x - playerX) * qTan(rayAngle + angleQuarter);
					
						// Cast horizontal ray
						while (true) {
							if (horizontal.x >= mapSize || horizontal.x < 0 || horizontal.y >= mapSize || horizontal.y < 0) break;
							horizontal.texture_Num = map[(int)horizontal.x][(int)horizontal.y-horizontal.adjY];
							if (horizontal.texture_Num<0) {
								horizontal.doorStatus = doorStatus[(int)horizontal.x][(int)horizontal.y-horizontal.adjY];
								horizontal.hitDoor = true;
								if (!horizontal.alreadyHitDoor) {
									horizontal.doorX = (int)horizontal.x;
									horizontal.doorY = (int)horizontal.y-horizontal.adjY;
									horizontal.alreadyHitDoor = true;
								}
								horizontal.x+=horizontal.dX/2;
								horizontal.y+=horizontal.dY/2;
							}
							if (horizontal.texture_Num>0 || ( (horizontal.texture_Num<0) && ((horizontal.x-(int)horizontal.x)<horizontal.doorStatus) )) break;
							if (horizontal.texture_Num < 0) {
								horizontal.doorStatus = 0;
								horizontal.x+=horizontal.dX/2;
								horizontal.y+=horizontal.dY/2;
							} else {
								horizontal.x+=horizontal.dX;
								horizontal.y+=horizontal.dY;
							}
						}
						horizontal.length = (playerX - horizontal.x)*(playerX - horizontal.x) + (playerY - horizontal.y)*(playerY - horizontal.y);

						// Cast vertical ray
						while (true) {
							if (vertical.x >= mapSize || vertical.x < 0 || vertical.y >= mapSize || vertical.y < 0) break;
							vertical.texture_Num = map[(int)vertical.x-vertical.adjX][(int)vertical.y];
							if (vertical.texture_Num<0) {
								vertical.hitDoor = true;
								vertical.doorStatus = doorStatus[(int)vertical.x-vertical.adjX][(int)vertical.y];
								if (!vertical.alreadyHitDoor) {
									vertical.doorX = (int)vertical.x-vertical.adjX;
									vertical.doorY = (int)vertical.y;
									vertical.alreadyHitDoor = true;
								}
								vertical.x+=vertical.dX/2;
								vertical.y+=vertical.dY/2;
							}
							if (vertical.texture_Num>0 || ( (vertical.texture_Num<0) && ((vertical.y-(int)vertical.y)<vertical.doorStatus) )) break;
							if (vertical.texture_Num < 0) {
								vertical.doorStatus = 0;
								vertical.x+=vertical.dX/2;
								vertical.y+=vertical.dY/2;
							} else {
								vertical.x+=vertical.dX;
								vertical.y+=vertical.dY;
							}
						}
						vertical.length = (playerX - vertical.x)*(playerX - vertical.x) + (playerY - vertical.y)*(playerY - vertical.y);
						
						
						ray realRay;

						if (horizontal.length<=vertical.length) {
							realRay = horizontal;			
						} else {
							realRay = vertical;
							realRay.x = realRay.y;
						}

						depthBuffer[r]=realRay.length;

						realRay.length = sqrt(realRay.length);

						texture_Num = abs(realRay.texture_Num);
						if (texture_Num!=0 && texture_Num<64 && textures[texture_Num]!=NULL) {
							int textureSize = textures[texture_Num]->w;
							wall_Dist = realRay.length;
							if (realRay.doorStatus>0 && realRay.doorStatus<1) {
								wall_Slice = (int)((realRay.x-(int)realRay.x + (1-realRay.doorStatus)) * textureSize)%textureSize;
							} else {
								wall_Slice = (int)((realRay.x-(int)realRay.x) * textureSize)%textureSize;
							}
							
							if (wall_Slice<0) break;
							wall_Dist = wall_Dist * qCos(r-scrWidth/2);
							s_Height = scrHeight/wall_Dist;
							drawTextureSlice(textures[texture_Num],screen,wall_Slice,r,s_Height);
						} else {
							printf("WARNING: Texture %i not found!\n",texture_Num);
						}


						// Floors! (and ceilings)
						// This is MEGA-SUPER-AWFUL-SLOW.

						if (rFloors) {
							if (s_Height<scrHeight) {

								int pixlX;
								int pixlY;
								int floorTexture = 1;
								int topOfScan=centreY+(s_Height>>1);
								int p = r*screen->format->BytesPerPixel;
								Uint8 *c = (Uint8*)screen->pixels+p;
								Uint8 *f = (Uint8*)screen->pixels+(scrHeight-1)*screen->pitch+p;
								int stepRow = screen->pitch;
								int endY = scrHeight-topOfScan;
								if (Slock(screen)) {
									for (int y=0; y<=endY; ++y) {
										double pixlDist = pixelDistTable[y+r*scrHeight];
										double flrX = pixlDist*qSin(rayAngle)+playerX;
										double flrY = pixlDist*qCos(rayAngle)+playerY;
										int floorTexture = mapFloors[(int)flrX][(int)flrY];
										int ceilingTexture = mapCeilings[(int)flrX][(int)flrY];
										if (floorTexture>0 && floorTexture<64 && textures[floorTexture]!=NULL) {
											pixlX = ((int)(flrX*128))%128;
											pixlY = ((int)(flrY*128))%128;
											*(Uint32*)f = *(Uint32*)((Uint8*)textures[floorTexture]->pixels+pixelLookup[pixlX][pixlY]);
										}
										if (ceilingTexture>0 && ceilingTexture<64 && textures[ceilingTexture]!=NULL) {
											pixlX = ((int)(flrX*textures[ceilingTexture]->w))%textures[ceilingTexture]->w;
											pixlY = ((int)(flrY*textures[ceilingTexture]->h))%textures[ceilingTexture]->h;
											*(Uint32*)c = *(Uint32*)((Uint8*)textures[ceilingTexture]->pixels+pixelLookup[pixlX][pixlY]);
					
										}
										f-=stepRow;
										c+=stepRow;
									}
									Sulock(screen);
								}
							}
						}
				

						// Doors!
					
						if (r==centreX) {
							if (horizontal.hitDoor) {
								lookingAtDoor = true;
								lookingDoorX = horizontal.doorX;
								lookingDoorY = horizontal.doorY;
							} else if (vertical.hitDoor) {
								lookingAtDoor = true;
								lookingDoorX = vertical.doorX;
								lookingDoorY = vertical.doorY;
							}
						}


					}
					
					// Objects!

					if (rObjects) {
						objects *sortObjects = new objects[numObjects];
						int visibleObjects=0;

						double camX = qSin(playerA);
						double camY = qCos(playerA);

						// Cull objects and create queue:

						for (int i=0; i<numObjects; ++i) {
							sortObjects[visibleObjects].offX = mapObjects[i].x - playerX;
							sortObjects[visibleObjects].offY = mapObjects[i].y - playerY;
							sortObjects[visibleObjects].yVecOffset = sortObjects[visibleObjects].offX *camX + sortObjects[visibleObjects].offY*camY;
							if (sortObjects[visibleObjects].yVecOffset>0) {
								sortObjects[visibleObjects].xVecOffset = sortObjects[visibleObjects].offX*qSin(playerA+angleQuarter) + sortObjects[visibleObjects].offY*qCos(playerA+angleQuarter);
								sortObjects[visibleObjects].model =  mapObjects[i].model;
								sortObjects[visibleObjects].animation = mapObjects[i].animation;
								sortObjects[visibleObjects].framePos =  mapObjects[i].framePos;
								sortObjects[visibleObjects].angle = mapObjects[i].angle;
								++visibleObjects;
							}
						}

						// Sort all objects into back-to-front order
						if (numObjects>1) {
							quickSort(sortObjects,0,numObjects-1);
						}

						// Step through each and draw it
						int spriteAngle;

						for (int i=0; i<visibleObjects; ++i) {
							spriteAngle = ((normaliseAngle(playerA-sortObjects[i].angle-angleHalf-angleQuarter/2)*8)/numAnglesInCircle)%8;
							int h = scrHeight/sortObjects[i].yVecOffset;
							if (h<=0) break;
							int rX = centreX-sortObjects[i].xVecOffset*(scrWidth/sortObjects[i].yVecOffset);
							int checkY = sortObjects[i].yVecOffset * sortObjects[i].yVecOffset;
							int sprOff = soldier->frameOffset[sortObjects[i].animation]+(sortObjects[i].framePos)*8 +spriteAngle;
							SDL_Surface *worldSprite = soldier->textures[sprOff];
							int textW = worldSprite->w;
							int sX=0;
							for (int x=rX-(h>>1); x<=rX+(h>>1); ++x) {
								if (x>=0 && x<scrWidth) {
									if (depthBuffer[x]>checkY) {
										drawTextureSlice(worldSprite,screen,(textW*sX)/h,x,h);
										depthBuffer[x]=checkY;
									}					
								}
								++sX;
							}
						}

						delete sortObjects;
					}


					++fpsCount;

					Sulock(screen);
			        
					// HUD

					SDL_BlitSurface(healthHUD, NULL, screen, &healthPos);
					drawNumber(screen, scrWidth-3*24,scrHeight-40, rFPS, 3);

					// Console:

					if (showConsole) {
						SDL_Rect consoleBack;
						consoleBack.w = scrWidth;
						consoleBack.h = fontPlaceHeight*(consoleNumLines+1)+1;
						consoleBack.x = 0;
						consoleBack.y = 0;
						SDL_FillRect(screen,&consoleBack,0x003300);
						for (int i=0; i<consoleNumLines; ++i) {
							consolePutString(screen,consoleLines[i],0,i*fontPlaceHeight);
						}
						SDL_Rect lastPos = consolePutString(screen, consoleTextInput, 0, consoleNumLines*fontPlaceHeight);
						consolePutC(screen, '_', lastPos.x, lastPos.y);
					}

					
					SDL_Flip(screen);
					SDL_Delay(1);
				}
			}
		}
	}

	printf("Shutting down SDL... ");
	SDL_Quit();
	printf("Done.\n");

	if (hitError) {
		printf("This program has quit due to an unexpected error.\n");
		system("pause");
	}

	return 0;
}
