#include "../Framework/Framework.h"
#include "../Framework/Util/T3dLoader.h"
#include "../Framework/Util/Particles.h"
#include "../Framework/Util/DemoLooper.h"

#define NO_MUSIC


#ifndef NO_MUSIC
#include "../Framework/Audio/Audio.h"
#endif

struct TexVertex {
	Vertex vertex;
	float s, t;
	Vertex sVec, tVec, normal;
};

struct State {
	Material *material;
	unsigned int indexIndex, nIndices;
};

class MainApp : public OpenGLApp {
protected:
	TextureID tex0, tex1, tex2;
	TextureID tex0Bump, tex1Bump, tex2Bump;
	TextureID vectorField, particle, corona, skyBox;

	ShaderID vertexShader;
	DemoLooper demoLooper;
	bool demoMode;

	Set <State> states;
	TexVertex *vertices;
	unsigned int *indices;
	unsigned int nVertices, nIndices;

#ifndef NO_MUSIC
	Audio *audio;
	SoundID music;
#endif

	GLuint query;
	float area;

    MaterialTable matTable;

	Set <ParticleSystem *> particleSystems;

	void drawWorld();
public:
	void selectPixelFormat(PixelFormat &pixelFormat);

	bool load();
	bool unload();
	bool init();
	bool exit();
	bool drawFrame();
	
	void setKey(UINT key, bool value);
	void setViewport(int cx, int cy);
	void resetCamera();
};

void MainApp::setKey(UINT key, bool value){
	OpenGLApp::setKey(key, value);

	if (value){
		if (key == demoModeKey){
            if (demoMode){
                demoMode = false;
            } else {
                if (demoLooper.getNumberOfNodes() > 0){
                    demoMode = true;
					//currTime = (float) (2 * demoLooper.getNumberOfNodes()) - 8;
                }
            }
		}

		/*switch(key){
        case '1':
			Wz += 0.01f;
            break;
        case '2':
			Wz -= 0.01f;
            break;
        case 'O':
            demoLooper.loadFromFile("path.pth");
            break;
        case 'S':
            demoLooper.writeToFile("path.pth");
            break;
		case 'C':
			demoLooper.clear();
			break;
        case VK_INSERT:
            demoLooper.addNode(position, Wx, Wy, Wz);
			break;
		case VK_BACK:
			demoLooper.removeLastNode();
            break;
		}*/		
	}	
}

void MainApp::setViewport(int cx, int cy){
	OpenGLApp::setViewport(cx, cy);
	area = (float) (cx * cy);
}

void MainApp::resetCamera(){
	position = Vertex(-1200, 0, 0);
	Wx = 0;
	Wy = -PI / 2;
	Wz = 0;
}

void MainApp::selectPixelFormat(PixelFormat &pixelFormat){
	pixelFormat.redBits   = 8;
	pixelFormat.greenBits = 8;
	pixelFormat.blueBits  = 8;
	pixelFormat.alphaBits = 0;

	pixelFormat.depthBits = 24;
}

bool polygonFilter(const int flags){
	return (flags & PF_Invisible) == 0 && (flags & PF_Translucent) == 0 && (flags >= 0);
}

int comp(class Polygon *const &elem0, class Polygon *const &elem1){
	return (elem0->getMaterial() - elem1->getMaterial());
}

bool MainApp::load(){
	String supported, missing;

    if (GL_NV_occlusion_query)     supported += "GL_NV_occlusion_query\n";     else missing += "GL_NV_occlusion_query\n";
    if (GL_ARB_vertex_program)     supported += "GL_ARB_vertex_program\n";     else missing += "GL_ARB_vertex_program\n";
    if (GL_ARB_texture_env_dot3)   supported += "GL_ARB_texture_env_dot3\n";   else missing += "GL_ARB_texture_env_dot3\n";
    if (GL_EXT_texture3D)          supported += "GL_EXT_texture3D\n";          else missing += "GL_EXT_texture3D\n";
    if (GL_EXT_texture_edge_clamp) supported += "GL_EXT_texture_edge_clamp\n"; else missing += "GL_EXT_texture_edge_clamp\n";

    if (missing.getLength() > 0){
        ALERT("The following required OpenGL extensions are supported:\n\n" + supported +
				   "\nToo bad, these required OpenGL extensions aren't supported by the graphic card and driver you're using:\n\n" + missing +
                   "\nThis application will now exit.", "Missing extensions");
        return false;
    }

	vectorField = renderer->addVectorField(64, TEX_NOMIPMAPPING | TEX_USE_16BIT);

	tex0     = renderer->addTexture("../Textures/Wood.dds");
	tex0Bump = renderer->addTexture("../Textures/WoodBump.png", TEX_NORMALMAP | TEX_USE_16BIT);
	tex1     = renderer->addTexture("../Textures/limestone.dds");
	tex1Bump = renderer->addTexture("../Textures/limestoneBump.png", TEX_NORMALMAP | TEX_USE_16BIT);
	tex2     = renderer->addTexture("../Textures/brickfloor.dds");
	tex2Bump = renderer->addTexture("../Textures/brickfloorBump.png", TEX_NORMALMAP | TEX_USE_16BIT);

	skyBox = renderer->addCubemap(
		"../Textures/CubeMaps/FireSky/negx.jpg",
		"../Textures/CubeMaps/FireSky/posx.jpg",
		"../Textures/CubeMaps/FireSky/negy.jpg",
		"../Textures/CubeMaps/FireSky/posy.jpg",
		"../Textures/CubeMaps/FireSky/negz.jpg",
		"../Textures/CubeMaps/FireSky/posz.jpg");



	particle = renderer->addTexture("../Textures/Particle.png");
	corona   = renderer->addTexture("../Textures/Corona2.png");

	textTexture = renderer->addTexture("../Textures/font2.dds");

	projection.loadProjectionMatrix(90 * PI / 180, 0.75f, 0.1f, 6000);

	vertexShader = renderer->addVertexShader("Lighting.vsh");

	if (glGetError() != GL_NO_ERROR){
		char *str = (char *) glGetString(GL_PROGRAM_ERROR_STRING_ARB);
		ALERT(str, "Error");
	}

	Material *mat;

	mat = new Material();
	mat->setTextures(tex0, tex0Bump);
    matTable.addMaterialName("nxnxbase", 256, 256, mat, 0);

	mat = new Material();
	mat->setTextures(tex1, tex1Bump);
    matTable.addMaterialName("xlamp13", 32, 64, mat, 0);

	mat = new Material();
	mat->setTextures(tex2, tex2Bump);
    matTable.addMaterialName("nufloorbase", 256, 256, mat, 0);

	Set <class Polygon *> polys;

	T3dLoader t3dLoader;
	t3dLoader.loadFromFile("map.t3d", polys, matTable, polyCreator, polygonFilter, Vertex(0,0,0));

	//fixTJunctions(polys);
	polys.sort(comp);

	unsigned int i, j, len = polys.getSize();
	
	nVertices = 0;
	for (i = 0; i < len; i++){
		nVertices += polys[i]->getnVertices();
	}
	nIndices = 3 * nVertices - 6 * len;

	vertices = new TexVertex[nVertices];
	indices = new unsigned int[nIndices];

	unsigned int *iDest = indices;
	unsigned int fvai, vai = 0;
	unsigned int last = 0, ni = 0;

	mat = polys[0]->getMaterial();
	i = 0;
	do {
		if (i == len || mat != polys[i]->getMaterial()){
			State s;
			s.material = mat;
			s.indexIndex = last;
			s.nIndices = ni - last;
			states.add(s);

			if (i == len) break;

			last = ni;
			mat = polys[i]->getMaterial();
		}

		fvai = vai;
		for (j = 0; j < polys[i]->getnVertices(); j++){
			if (j > 2){
				*iDest++ = fvai;
				*iDest++ = vai - 1;
			}
			*iDest++ = vai;

			vertices[vai].vertex = polys[i]->getVertex(j);
			vertices[vai].s = (polys[i]->getVertex(j) - polys[i]->getOrigin()) * (polys[i]->getS() * polys[i]->getScaleS());
			vertices[vai].t = (polys[i]->getVertex(j) - polys[i]->getOrigin()) * (polys[i]->getT() * polys[i]->getScaleT());
			vertices[vai].sVec = polys[i]->getS();
			vertices[vai].tVec = polys[i]->getT();
			vertices[vai].normal = polys[i]->getPlane().normal;

			vai++;
		}
		ni += 3 * (polys[i]->getnVertices() - 2);
		
		delete polys[i];

		i++;
	} while (true);


	glGenOcclusionQueriesNV(1, &query);

	return true;
}

bool MainApp::unload(){
	delete indices;
	delete vertices;

	states.clear();

	glDeleteOcclusionQueriesNV(1, &query);

	return true;
}

bool MainApp::init(){
	ParticleSystem *particles;

	particles = new ParticleSystem();
	particles->setSpawnRate(30);
	particles->setSpeed(150, 20, 0.3f, 0.3f);
	particles->setLife(2.0f, 0);
	particles->setDirectionalForce(Vertex(0,-10,0));
	particles->setFrictionFactor(0.7f);
	particles->setPosition(Vertex(0,-70,580));
	particles->setSize(30, 10);
	particleSystems.add(particles);

	particles = new ParticleSystem();
	particles->setSpawnRate(30);
	particles->setSpeed(150, 20, 0.3f, 0.3f);
	particles->setLife(2.0f, 0);
	particles->setDirectionalForce(Vertex(0,-10,0));
	particles->setFrictionFactor(0.7f);
	particles->setPosition(Vertex(0,-70,-580));
	particles->setSize(30, 10);
	particleSystems.add(particles);

	demoMode = demoLooper.loadFromFile("Path.pth");
	demoLooper.setClampMode(false);

#ifndef NO_MUSIC

	audio = new Audio();

	music = audio->addSound("Antissa.ogg", SND_STREAM | SND_LOOPING);
#endif

	return true;
}

bool MainApp::exit(){
#ifndef NO_MUSIC
	delete audio;
#endif

	int i, len = particleSystems.getSize();
	for (i = 0; i < len; i++) delete particleSystems[i];
	particleSystems.clear();

	return true;
}

bool MainApp::drawFrame(){
#ifndef NO_MUSIC
	static bool first = true;

	if (first){
		audio->playSound(music);
		first = false;
	}
	audio->updateSoundStream(music);
#endif

	if (demoMode){
		LoopNode node =	demoLooper.getNode(currTime / 2);

		position = Vertex(node.x, node.y, node.z);
		Wx = node.Wx;
		Wy = node.Wy;
		Wz = node.Wz;
	}

	unsigned int i, j;

	glMatrixMode(GL_PROJECTION);
	glLoadMatrixf(projection);

	modelView.loadRotateZXY(-Wx, -Wy, -Wz);
    glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(modelView);

	glClear(GL_DEPTH_BUFFER_BIT);
	glDisable(GL_DEPTH_TEST);

	renderer->setTextures(skyBox);
	renderer->setTexEnvs(TEXENV_REPLACE);
	renderer->apply();

	glBegin(GL_TRIANGLE_STRIP);
		glTexCoord3f(-1, -1, -1);
		glVertex3f  (-1, -1, -1);
		glTexCoord3f(-1,  1, -1);
		glVertex3f  (-1,  1, -1);

		glTexCoord3f(-1, -1,  1);
		glVertex3f  (-1, -1,  1);
		glTexCoord3f(-1,  1,  1);
		glVertex3f  (-1,  1,  1);

		glTexCoord3f( 1, -1,  1);
		glVertex3f  ( 1, -1,  1);
		glTexCoord3f( 1,  1,  1);
		glVertex3f  ( 1,  1,  1);

		glTexCoord3f( 1, -1, -1);
		glVertex3f  ( 1, -1, -1);
		glTexCoord3f( 1,  1, -1);
		glVertex3f  ( 1,  1, -1);
	glEnd();
	glBegin(GL_TRIANGLE_STRIP);
		glTexCoord3f(-1, -1,  1);
		glVertex3f  (-1, -1,  1);
		glTexCoord3f( 1, -1,  1);
		glVertex3f  ( 1, -1,  1);

		glTexCoord3f(-1, -1, -1);
		glVertex3f  (-1, -1, -1);
		glTexCoord3f( 1, -1, -1);
		glVertex3f  ( 1, -1, -1);

		glTexCoord3f(-1,  1, -1);
		glVertex3f  (-1,  1, -1);
		glTexCoord3f( 1,  1, -1);
		glVertex3f  ( 1,  1, -1);

		glTexCoord3f(-1,  1,  1);
		glVertex3f  (-1,  1,  1);
		glTexCoord3f( 1,  1,  1);
		glVertex3f  ( 1,  1,  1);
	glEnd();

	modelView.translate(-position);
    glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(modelView);

	glDepthFunc(GL_LEQUAL);
	glEnable(GL_DEPTH_TEST);

	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);

	glEnableClientState(GL_VERTEX_ARRAY);
	glVertexPointer(3, GL_FLOAT, sizeof(TexVertex), vertices);

	glClientActiveTextureARB(GL_TEXTURE0_ARB);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glTexCoordPointer(2, GL_FLOAT, sizeof(TexVertex), ((char *) vertices) + sizeof(Vertex));

	glClientActiveTextureARB(GL_TEXTURE1_ARB);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glTexCoordPointer(3, GL_FLOAT, sizeof(TexVertex), ((char *) vertices) + sizeof(Vertex) + 2 * sizeof(float));

	glClientActiveTextureARB(GL_TEXTURE2_ARB);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glTexCoordPointer(3, GL_FLOAT, sizeof(TexVertex), ((char *) vertices) + 2 * sizeof(Vertex) + 2 * sizeof(float));

	glEnableClientState(GL_NORMAL_ARRAY);
	glNormalPointer(GL_FLOAT, sizeof(TexVertex), ((char *) vertices) + 3 * sizeof(Vertex) + 2 * sizeof(float));


	for (j = 0; j < 2; j++){
		glProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, 0, 0, -10, (j == 0)? -580.0f: 580.0f, 0);

		float size = 0.0004f + 0.000015f * cosf(currTime * (j * 6 + 11)) * sinf(currTime * (13 - 2 * j));
		glProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, 1, size, size, size, size);

		if (j == 1) glDepthMask(GL_FALSE);

		for (i = 0; i < states.getSize(); i++){
			renderer->setTextures(states[i].material->textures[1], vectorField);
			renderer->setTexEnvs(TEXENV_REPLACE, TEXENV_DOT3);
			renderer->setVertexShader(vertexShader);
			if (j > 0) renderer->setBlending(GL_ONE, GL_ONE);
			renderer->apply();

			glDrawElements(GL_TRIANGLES, states[i].nIndices, GL_UNSIGNED_INT, indices + states[i].indexIndex);
		}
	}

	glDisableClientState(GL_NORMAL_ARRAY);
	glClientActiveTextureARB(GL_TEXTURE2_ARB);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glClientActiveTextureARB(GL_TEXTURE1_ARB);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glColor3f(1,1,1);

	for (i = 0; i < states.getSize(); i++){
		renderer->setTextures(states[i].material->textures[0]);
		renderer->setBlending(GL_DST_COLOR, GL_SRC_COLOR);
		renderer->apply();

		glDrawElements(GL_TRIANGLES, states[i].nIndices, GL_UNSIGNED_INT, indices + states[i].indexIndex);
	}

	glClientActiveTextureARB(GL_TEXTURE0_ARB);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glDisableClientState(GL_VERTEX_ARRAY);


	Vertex dx(modelView[0], modelView[4], modelView[8]);
	Vertex dy(modelView[1], modelView[5], modelView[9]);

	for (i = 0; i < particleSystems.getSize(); i++){
		renderer->setTextures(particle);
		renderer->setBlending(GL_ONE, GL_ONE);
		renderer->apply();

		glEnable(GL_DEPTH_TEST);

		particleSystems[i]->update(currTime);
		glBeginOcclusionQueryNV(query);
			particleSystems[i]->draw(dx, dy);
	    glEndOcclusionQueryNV();

		glFinish();

		int nPixels = 0;
		glGetOcclusionQueryivNV(query, GL_PIXEL_COUNT_NV, &nPixels);

		renderer->setTextures(corona);
		renderer->setBlending(GL_ONE, GL_ONE);
		renderer->apply();

		glColor3fv(Color(1,0.8f,0.1f) * (1.0f - expf(-nPixels * 20 / area)));

		glDisable(GL_DEPTH_TEST);

		Vertex v = particleSystems[i]->getPosition() + Vertex(0,60,0);
		float size = 500 + 10 * cosf(currTime * 9 + i) * sinf(currTime * 7 - i);
		glBegin(GL_QUADS);
			glTexCoord2f(0,0);
			glVertex3fv(v + size * (-dx + dy));
			glTexCoord2f(1,0);
			glVertex3fv(v + size * ( dx + dy));
			glTexCoord2f(1,1);
			glVertex3fv(v + size * ( dx - dy));
			glTexCoord2f(0,1);
			glVertex3fv(v + size * (-dx - dy));
		glEnd();
	}
	glDepthMask(GL_TRUE);
	glDisable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);

	return true;
}

DECLARE_MAIN_APP(MainApp)
