#include <AL/al.h>
#include <AL/alut.h>
#include <vorbis/vorbisfile.h>
#include <SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>



#define LENGTHOF(X) (sizeof(X)/sizeof((X)[0]))



typedef Uint32      GAL_Source;
typedef const void* GAL_Tag;
typedef const char* stringz;



typedef struct
{
	double         loopPosition;
	ALenum         format;
	ALsizei        frequency;
	OggVorbis_File file;
}GAL_Music;



typedef struct
{
	ALuint buffer;
}GAL_Sound;



typedef enum
{
	NONE,
	PLAY,
	STOP,
	QUIT,
}OperationID;



typedef union
{
	OperationID id;

	struct
	{
		OperationID id;
		GAL_Music*  music;
	}play;
}Operation;



static unsigned int soundInstanceCount = 0;



static struct
{
	SDL_Thread* thread;
	struct
	{
		GAL_Music* stream;
		ALuint     buffers[4];
		ALuint     source;
	}music;

	struct
	{
		GAL_Source count;
		struct
		{
			ALuint name;
			GAL_Tag    tag;
		}items[8];
	}sounds;
}mixer;



static struct
{
	SDL_mutex* mutex[2];
	Operation  operation;
}mail;



static void createMail(void)
{
	mail.mutex[0] = SDL_CreateMutex();
	mail.mutex[1] = SDL_CreateMutex();
	SDL_mutexP(mail.mutex[1]);
	mail.operation.id = NONE;
}


static void releaseMail(void)
{
	SDL_mutexV(mail.mutex[1]);
	SDL_DestroyMutex(mail.mutex[0]);
	SDL_DestroyMutex(mail.mutex[1]);
	memset(&mail, 0, sizeof(mail));
}


static void sendMail(const Operation* operation)
{
	if(SDL_mutexP(mail.mutex[0]) == 0)
	{
		SDL_mutexP(mail.mutex[1]);
		mail.operation = *operation;
	}
}


static int pollMail(Operation* operation)
{
	if(mail.operation.id != NONE)
	{
		*operation = mail.operation;

		mail.operation.id = NONE;

		SDL_mutexV(mail.mutex[0]);

		return 1;
	}

	return 0;
}


static void replyMail(void)
{
	SDL_mutexV(mail.mutex[1]);
}



static void stop(void)
{
	alSourceStop(mixer.music.source);
	{
		int queued;

		alGetSourcei(mixer.music.source, AL_BUFFERS_QUEUED, &queued);

		while(queued--)
		{
			ALuint buffer;

			alSourceUnqueueBuffers(mixer.music.source, 1, &buffer);
		}
	}

	mixer.music.stream = NULL;
}


static int readSub(OggVorbis_File* file, char* buffer, size_t bufsize, long* size)
{
	long sum = 0;

	*size = 0;

	while(sum + 4*1024 <= bufsize)
	{
		static const int ENDIAN = 0; // 0 for Little-Endian, 1 for Big-Endian

		int  bitStream;

		// Read up to a buffer's worth of decoded sound data
		long bytes = ov_read(file, buffer+sum, bufsize-sum, ENDIAN, 2, 1, &bitStream);
		if(bytes > 0)
		{
			sum += bytes;
		}
		else if(bytes == 0)
		{
			break;
		}
		else
		{
			return -1;
		}
	}

	*size = sum;

	return 0;
}


static int read(ALuint buffer)
{
	static char readBuffer[32 * 1024];

	long size;

	if(readSub(&mixer.music.stream->file, readBuffer, sizeof(readBuffer), &size) == 0)
	{
		if(size == 0)
		{
			if(ov_time_seek(&mixer.music.stream->file, mixer.music.stream->loopPosition) == 0)
			{
				if(readSub(&mixer.music.stream->file, readBuffer, sizeof(readBuffer), &size))
				{
					size = 0;
				}
			}
		}
		if(size > 0)
		{
			alBufferData(buffer, mixer.music.stream->format, readBuffer, size, mixer.music.stream->frequency);

			return 0;
		}
	}

	fprintf(stderr, "bgm read error\n");

	return -1;
}


static void play(void)
{
	size_t i;

	for(i=0;i<LENGTHOF(mixer.music.buffers);++i)
	{
		if(read(mixer.music.buffers[i]) != 0)
		{
			return;
		}
	}

	alSourceQueueBuffers(mixer.music.source, LENGTHOF(mixer.music.buffers), mixer.music.buffers);

	alSourcePlay(mixer.music.source);
}


static void quit(void)
{
	GAL_Source i;

	alGetError();
	for(i=0; i<mixer.sounds.count; ++i)
	{
		alDeleteSources(1, &mixer.sounds.items[i].name);
		if(alGetError() != AL_NO_ERROR)
		{
			fprintf(stderr, "gal: couldn't delete sound source!\n");
		}
	}

	alDeleteSources(1, &mixer.music.source);
	if(alGetError() != AL_NO_ERROR)
	{
		fprintf(stderr, "gal: couldn't delete music source!\n");
	}
	alDeleteBuffers(LENGTHOF(mixer.music.buffers), mixer.music.buffers);

	alutExit();

	memset(&mixer, 0, sizeof(mixer));
}


static int mixerThread(void* unused)
{
	while(1)
	{
		Operation operation;
		if(pollMail(&operation))
		{
			switch(operation.id)
			{
			case STOP:
				stop();
				replyMail();
				break;

			case PLAY:
				stop();

				mixer.music.stream = operation.play.music;

				replyMail();

				play();
				break;

			case QUIT:
				stop();
				replyMail();
				return 0;

			default:
				replyMail();
				break;
			}
		}
		else if(mixer.music.stream)
		{
			ALint state;
			int   processed;

			alGetSourcei(mixer.music.source, AL_BUFFERS_PROCESSED, &processed);

			while(processed--)
			{
				ALuint buffer;

				alSourceUnqueueBuffers(mixer.music.source, 1, &buffer);

				if(read(buffer) == 0)
				{
					alSourceQueueBuffers(mixer.music.source, 1, &buffer);
				}
			}

			alGetSourcei(mixer.music.source, AL_SOURCE_STATE, &state);
			if(state != AL_PLAYING)
			{
				alSourcePlay(mixer.music.source);
			}
		}
	}
}


void GAL_initialize(int argc, stringz* argv)
{
	memset(&mixer, 0, sizeof(mixer));

	// Initialize the OpenAL library
	alutInit(&argc, (char**)argv);

	alGenSources(1, &mixer.music.source);
	alGenBuffers(LENGTHOF(mixer.music.buffers), mixer.music.buffers);

	alGetError();
	for(mixer.sounds.count=0; mixer.sounds.count<LENGTHOF(mixer.sounds.items); ++mixer.sounds.count)
	{
		alGenSources(1, &mixer.sounds.items[mixer.sounds.count].name);
		if(alGetError() != AL_NO_ERROR)
		{
			break;
		}
	}

	mixer.thread = SDL_CreateThread(mixerThread, NULL);

	createMail();
}


void GAL_quit(void)
{
	Operation operation;

	operation.id = QUIT;

	sendMail(&operation);

	SDL_WaitThread(mixer.thread, NULL);

	releaseMail();

	mixer.thread = NULL;

	if(soundInstanceCount == 0)
	{
		quit();
	}
}



GAL_Music* GAL_createMusic(stringz fileName)
{
	GAL_Music* music = (GAL_Music*)calloc(1, sizeof(*music));

	if(music)
	{
		// Try opening the given file
		if(ov_fopen((char*)fileName, &music->file) == 0)
		{
			// Get some information about the OGG file
			vorbis_info* pInfo = ov_info(&music->file, -1);

			// Check the number of channels... always use 16-bit samples
			if(pInfo->channels == 1)
			{
				music->format = AL_FORMAT_MONO16;
			}
			else
			{
				music->format = AL_FORMAT_STEREO16;
			}

			// The frequency of the sampling rate
			music->frequency = pInfo->rate;

			return music;
		}
		else
		{
			fprintf(stderr, "%s: error opening for decoding...\n", fileName);
		}

		free(music);
		music = NULL;
	}
	else
	{
		fprintf(stderr, "createMusic: no memory...\n");
	}

	return NULL;
}


void GAL_stopMusic(void)
{
	Operation operation;

	operation.id = STOP;

	sendMail(&operation);
}


void GAL_releaseMusic(GAL_Music** music)
{
	if(*music)
	{
		if(mixer.music.stream == *music)
		{
			GAL_stopMusic();
		}

		// Clean up!
		ov_clear(&(*music)->file);

		free(*music);

		*music = NULL;
	}
}


void GAL_playMusic(GAL_Music* music)
{
	Operation operation;

	operation.id         = PLAY;
	operation.play.music = music;

	sendMail(&operation);
}



GAL_Sound* GAL_createSound(stringz fileName)
{
	GAL_Sound* sound;
	int        isOK = 0;

	sound = (GAL_Sound*)calloc(1, sizeof(*sound));

	if(sound)
	{
		OggVorbis_File oggFile;

		// Try opening the given file
		if(ov_fopen((char*)fileName, &oggFile) == 0)
		{
			char*   data = NULL;
			ALenum  format;
			ALsizei frequency;
			ALsizei size = 0;

			// Get some information about the OGG file
			vorbis_info* pInfo = ov_info(&oggFile, -1);

			// Check the number of channels... always use 16-bit samples
			if(pInfo->channels == 1)
			{
				format = AL_FORMAT_MONO16;
			}
			else
			{
				format = AL_FORMAT_STEREO16;
			}

			// The frequency of the sampling rate
			frequency = pInfo->rate;

			// Keep reading until all is read
			while(1)
			{
				static char array[32*1024]; // Local fixed size array

				long bytes;

				// Read up to a buffer's worth of decoded sound data
				if(readSub(&oggFile, array, sizeof(array), &bytes) == 0)
				{
					if(bytes > 0)
					{
						char* newBuffer = (char*)malloc(size + bytes);

						memcpy(newBuffer, data, size);

						memcpy(newBuffer + size, array, bytes);

						free(data);

						data  = newBuffer;
						size += bytes;
					}
					else
					{
						alGetError();
						alGenBuffers(1, &sound->buffer);
						if(alGetError() == AL_NO_ERROR)
						{
							alBufferData(sound->buffer, format, data, size, frequency);
							if(alGetError() == AL_NO_ERROR)
							{
								isOK = 1;
								++soundInstanceCount;
							}
							else
							{
								alDeleteBuffers(1, &sound->buffer);
							}
						}
						break;
					}
				}
				else
				{
					fprintf(stderr, "%s: decode error...\n", fileName);
					break;
				}
			}

			// Clean up!
			ov_clear(&oggFile);

			free(data);
			data = NULL;
		}
		else
		{
			fprintf(stderr, "%s: error opening for decoding...\n", fileName);
		}

		if(!isOK)
		{
			free(sound);
			sound = NULL;
		}
	}

	return sound;
}


void GAL_releaseSound(GAL_Sound** sound)
{
	if(*sound)
	{
		ALint buffer;
		GAL_Source i;

		for(i=0; i<mixer.sounds.count; ++i)
		{
			alGetSourcei(mixer.sounds.items[i].name, AL_BUFFER, &buffer);

			if(buffer == (*sound)->buffer)
			{
				alSourceStop(mixer.sounds.items[i].name);

				// Attach sound buffer to source
				alSourcei(mixer.sounds.items[i].name, AL_BUFFER, AL_NONE);
			}
		}

		alDeleteBuffers(1, &(*sound)->buffer);

		free(*sound);

		*sound = NULL;

		--soundInstanceCount;

		if(mixer.thread == NULL && soundInstanceCount == 0)
		{
			quit();
		}
	}
}


GAL_Source GAL_playSound(GAL_Sound* sound, GAL_Tag tag)
{
	ALint state;    // The state of the sound source
	GAL_Source i;

	alGetError();
	for(i=0; i<mixer.sounds.count; ++i)
	{
		// Query the state of the souce
		alGetSourcei(mixer.sounds.items[i].name, AL_SOURCE_STATE, &state);
		if(alGetError() == AL_NO_ERROR)
		{
			switch(state)
			{
			case AL_INITIAL:
			case AL_STOPPED:
				// Attach sound buffer to source
				mixer.sounds.items[i].tag = tag;
				alSourcei(mixer.sounds.items[i].name, AL_BUFFER, sound->buffer);
				if(alGetError() == AL_NO_ERROR)
				{
					alSourcePlay(mixer.sounds.items[i].name);
					if(alGetError() == AL_NO_ERROR)
					{
						return i;
					}
					else
					{
						fprintf(stderr, "couldn't play sound\n");
						return 0;
					}
				}
				else
				{
					fprintf(stderr, "couldn't attach buffer to source\n");
					return 0;
				}
				break;

			default:
				break;
			}
		}
		else
		{
			fprintf(stderr, "couldn't get source state\n");
			return 0;
		}
	}

	return 0;
}


void GAL_stopSound(GAL_Source i, GAL_Tag tag)
{
	if(mixer.sounds.items[i].tag == tag)
	{
		alSourceStop(mixer.sounds.items[i].name);
	}
}

