//Fireworks demo.
//By Michael Barnathan.
//Written from scratch.

//We'll use glut.h to write portable code, unlike what the book does.
#include <iostream>
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alut.h>
#include <GL/glut.h>
#include "BMPLoader.h"	//This is library code.
#include "FireworksSystem.h"
#include "OggFile.h"

using namespace std;
using namespace ALWrapper;

const unsigned int numbuffers = 4;
const ALsizei bufsize = 16384;	//Size of a buffer. The sample uses this size.
const char* audiofile = "music/AllaHornpipe.ogg";

bool aborting = false;

ALuint buffer[numbuffers];
ALenum lasterror;
ALenum format;		//Format of the audio. This is in vfileinfo.
ALsizei freq;		//Frequency in KHz.
ALuint source;
ALfloat sourcePos[] = { 0.0, 0.0, 0.0 };
ALfloat sourceVel[] = { 0.0, 0.0, 0.0 };
ALfloat rolloff = 0.0;
ALfloat listenerPos[] = { 0.0, 0.0, 0.0 };
ALfloat listenerVel[] = { 0.0, 0.0, 0.0 };
ALfloat listenerOri[] = { 0.0, 0.0, -1.0, 0.0, 1.0, 0.0 };
bool bufvalid = false;

OggFile infile;

int windowID = 0;
float angle = 0.0f;
GLuint texture[1];					// Storage For Our Particle Texture
FireworksSystem fireworks(10000);

void changeSize(int w, int h);
void displayCallback();
void idleCallback();
void keyCallback(unsigned char key, int x, int y);

int InitializeOpenAL();
void InitializeOpenGL();
void CleanupOpenAL();
void LoadGLTextures();
bool fillbuffer(OggFile& invorbis, ALuint& buf, ALenum format, ALsizei freq);
bool playing(ALuint& source);

int main(int argc, char** argv) {
	alutInit(&argc, argv);
	alGetError();	//Clear the current error. Clumsy API.

	InitializeOpenAL();

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
	glutInitWindowPosition(100,100);
	glutInitWindowSize(620,620);

	windowID = glutCreateWindow("Fireworks");
	glutReshapeFunc(changeSize);
	glutDisplayFunc(displayCallback);
	glutIdleFunc(idleCallback);
	glutKeyboardFunc(keyCallback);
	
	InitializeOpenGL();

	//This blocks.
	glutMainLoop();

	return 0;
}

void displayCallback() {
	if (aborting)
		return;

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glPushMatrix();

	//glRotatef(angle, 0.0f, 1.0f, 0.0f);
	angle += 0.01f;

	//Redraw the fireworks.
	fireworks.update();

	glPopMatrix();

	//Never has double buffering been so easy.
	glutSwapBuffers();
}

int InitializeOpenAL() {
	//Create our buffers.
	alGenBuffers(numbuffers, buffer);

	//Check for errors.
	if ((lasterror = alGetError()) != AL_NO_ERROR)
		cerr << "Error " << lasterror << " while generating buffer"
		<< endl;
	
	//Load a file into memory. Vorbis wants FILE*. Open read-binary.
	try {
		infile.open(audiofile);
	}
	catch (AudioFileError e) {
		cerr << "Error while opening " << audiofile << ": " << e.what() << endl;
		return 1;
	};

	//Get info from current stream location in file.
	freq = infile.getFrequency();
	cout << "Filename: " << infile.getFilename() << endl;
	cout << "Size: " << infile.getSize() << " bytes.\tLength:";
	cout << (unsigned long) infile.getLength() / 3600 << ':'
		 << ((unsigned long) infile.getLength() % 3600) / 60 << ':'
		 << ((unsigned long) infile.getLength() % 3600) % 60 << endl;
	cout << "Frequency: " << infile.getFrequency() << " KHz" << endl;
	cout << "Average Bitrate: " << infile.getBitRate() << " bps" << endl;
	cout << "Channels: " << infile.getChannels() << endl;
	cout << "Comments: " << endl << infile.getComments() << endl;

	if (infile.getChannels() == 1) {	//One channel is mono.
		format = AL_FORMAT_MONO16;
		cout << "Using 16 bit mono playback." << endl;
	}
	else {				//Multiple channels are stereo.
		format = AL_FORMAT_STEREO16;
		cout << "Using 16 bit stereo playback." << endl;
	}
	
	//Now for our sources...
	alGenSources(1, &source);
	alSourcei(source, AL_LOOPING, AL_FALSE);

	//Set our parameters.
	alSourcefv(source, AL_POSITION, sourcePos);
	alSourcefv(source, AL_VELOCITY, sourceVel);
	alSourcef (source, AL_ROLLOFF_FACTOR, rolloff);
	alSourcei (source, AL_SOURCE_RELATIVE, AL_TRUE);

	//Now for the listener.	
	alListenerfv(AL_POSITION, listenerPos);
	alListenerfv(AL_VELOCITY, listenerVel);
	alListenerfv(AL_ORIENTATION, listenerOri);

	//Do one check for errors for all of that stuff.
	if ((lasterror = alGetError()) != AL_NO_ERROR) {
		cerr << "Error " << lasterror << " while setting parameters."
		<< endl;
		alDeleteBuffers(numbuffers, buffer);
		return 1;
	}

	//Fill all buffers.
	unsigned int bufcount = 0;	//Number of buffers with data.
	for (unsigned int bufindex = 0; bufindex < numbuffers; bufindex++)
		if (fillbuffer(infile, buffer[bufindex], format, freq))
			bufcount++;
	
	if (bufcount == 0) {
		cerr << "No buffers were filled." << endl;
		infile.close();
		return 0;
	}
	
	//Queue the buffers up.
	alSourceQueueBuffers(source, bufcount, buffer);
	if ((lasterror = alGetError()) != AL_NO_ERROR)
		cerr << "Error " << lasterror << " while queueing buffers." << endl;
	
	//Now we play the file!
	alSourcePlay(source);
	if ((lasterror = alGetError()) != AL_NO_ERROR)
		cerr << "Error " << lasterror << " while playing." << endl;

	bufvalid = true;
	return 0;
}

void idleCallback() {

	if (aborting)
		return;

	//Draw, then play.
	displayCallback();

	if (bufvalid) {
		//Find out how many buffers have been processed.
		ALint processed;
		alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);

		for (ALint bufindex = 0; bufindex < processed; bufindex++) {
			alSourceUnqueueBuffers(source, 1, &buffer[bufindex]);

			bufvalid = fillbuffer(infile, buffer[bufindex],
			format, freq);
			if (!bufvalid)
				//We hit the end (or there was an error).
				return;

			alSourceQueueBuffers(source, 1, &buffer[bufindex]);
			
		}

		if (!bufvalid)
			return;

		if (!playing(source)) {
			//Streaming was interrupted. Fill the buffers and
			//resume playback.
			cout << "Playback stopped." << endl;

			//Handle it smoothly by unqueuing the buffers.
			alSourceStop(source);
			alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
			alSourceUnqueueBuffers(source, processed, buffer);
			
			//Requeue.
			unsigned int bufcount = 0;	//Number of buffers with data.
			for (unsigned int bufindex = 0; bufindex < numbuffers;
			bufindex++)
				if (fillbuffer(infile, buffer[bufindex],
				format, freq))
					bufcount++;

			if (bufcount == 0)
				//I suppose we're done.
				return;

			alSourceQueueBuffers(source, bufcount, buffer);

			//Wait until playback has started again.
			alSourcePlay(source);

//			while (!playing(source));
				
		}
	}
}

void keyCallback(unsigned char key, int x, int y) {
	if (key == 27) {
		//Escape was pressed.
		cout << "Escape pressed; exiting!" << endl;
		aborting = true;
		CleanupOpenAL();
		glutDestroyWindow(windowID);

		exit(0);
	}
}

bool fillbuffer(OggFile& invorbis, ALuint& buf, ALenum format,
ALsizei freq) {
	//Fills a buffer up with ogg data.
	//Returns true on success.
	
	char data[bufsize];	//Temporary storage used to fill a buffer.
/*
	unsigned int sz = 0;	//Size currently filled.
	const int bigendian = 0;//Little endian on this machine. In a real
				//class, this would be determined by autoconf.
	const int wordsize = 2;	//We're using 16 bit samples.
	const int ov_nosign = 1;//Unsigned data.
	int curpos;		//Current position in the stream.

	//Fill a buffer.
	while (sz < bufsize) {
		long readres = ov_read(&invorbis, data+sz, bufsize - sz,
		bigendian, wordsize, ov_nosign, &curpos);
		if (readres > 0)
			//Success.
			sz += readres;
		else if (readres == 0)
			break;	//We've hit the end.
		else {
			cerr << strerror(readres) <<" while decoding!" << endl;
			return false;
		}
	}
	
	if (sz == 0)
		//Empty buffer?
		return false;
*/
	string tempbuf;
	try {
		tempbuf = invorbis.readData(bufsize);
	}
	catch (AudioFileError) {
		return false;
	}

	//Buffer the data.
	assert(tempbuf.length() <= bufsize);
	memcpy(data, tempbuf.data(), tempbuf.length());
	alBufferData(buf, format, data, tempbuf.length(), freq);
	
	return !tempbuf.empty();
}

bool playing(ALuint& source) {
	//Check if the stream is still playing.
	//It would have helped me greatly had I known about this earlier.
	
	ALenum state = 0;
	
	alGetSourcei(source, AL_SOURCE_STATE, &state);
	return (state == AL_PLAYING);
}


void InitializeOpenGL() {
	LoadGLTextures();

	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	glEnable(GL_BLEND);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D,texture[0]);			// Select Our Texture

	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
}

void LoadGLTextures()
{
	BMPClass bmp;
	BMPLoad("Particle.bmp", bmp);

    glEnable(GL_TEXTURE_2D);
	
	glBindTexture(GL_TEXTURE_2D, texture[0]);
        
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D,0,3,bmp.width,bmp.height,0,GL_RGB,GL_UNSIGNED_BYTE,bmp.bytes);
}

void CleanupOpenAL() {
	//Clean up!

	cout << "Cleaning up OpenAL..." << endl;
	while (playing(source))
		alSourceStop(source);
	
	infile.close();
	alDeleteSources(1, &source);
	alDeleteBuffers(numbuffers, buffer);
	alutExit();

}

//This function is copied from http://www.lighthouse3d.com/opengl/glut/index.php?3, as an example of how to resize a GLUT window.
void changeSize(int w, int h) {
	// Prevent a divide by zero, when window is too short
	// (you cant make a window of zero width).
	if(h == 0)
		h = 1;

	float ratio = 1.0* w / h;

	// Reset the coordinate system before modifying
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	
	// Set the viewport to be the entire window
	glViewport(0, 0, w, h);

	// Set the correct perspective.
	gluPerspective(45,ratio,1,1000);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(0.0,0.0,5.0, 
		      0.0,0.0,-1.0,
			  0.0f,1.0f,0.0f);


}

