#include "Audio.h"

/* Disables all ogg library dependency, and as a result, the music */
//#define AUDIO_NO_MUSIC

#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>

#ifndef AUDIO_NO_MUSIC
#define OV_EXCLUDE_STATIC_CALLBACKS
#include <ogg/ogg.h>
#include <vorbis/vorbisfile.h>
#endif

#include "SDL.h"

#include "ExMath.h"
#include "Log.h"
#include "Game.h" /* For Game.options.musicVolume, should be removed once there is a better way of changing music streams */
#include "rovdefs.h"
#include "World.h"

#define AUDIO_MAX_3D_SOUNDS 32
#define AUDIO_MAX_UI_SOUNDS 4 /* For sounds such as player damage, etc. with no 3D panning/atten. */

#define AUDIO_MAX_LOADED 128 /* Maximum number of sound files able to be loaded */
#define AUDIO_MAX_DISTANCE 20.0f

#define CLEAR_AL_ERRORS() alGetError()
#ifdef NDEBUG
#define CHECK_AL_ERRORS(why) alGetError()
#else
#define CHECK_AL_ERRORS(why) CheckALError(__FILE__, __LINE__, why)
static void CheckALError(const char *file, int line, const char *why)
{
	ALenum error = alGetError();
	if (error != AL_NO_ERROR)
	{
		char *errmsg;
		switch (error)
		{
		case AL_INVALID_NAME:
			errmsg = "AL_INVALID_NAME";
			break;
		case AL_INVALID_ENUM:
			errmsg = "AL_INVALID_ENUM";
			break;
		case AL_INVALID_VALUE:
			errmsg = "AL_INVALID_VALUE";
			break;
		case AL_INVALID_OPERATION:
			errmsg = "AL_INVALID_OPERATION";
			break;
		case AL_OUT_OF_MEMORY:
			errmsg = "AL_OUT_OF_MEMORY";
			break;
		default:
			errmsg = "Unknown";
			break;
		}
		Msg_Error("OpenAL Error at %s:%d (%s): %x %s", file, line, why, error, errmsg);
	}
}
#endif

struct WAVHeader
{
	/* "RIFF" chunk descriptor */
	char     ChunkID[4];
	uint32_t ChunkSize;
	uint32_t Format;
	/* "fmt" sub-chunk */
	char     Subchunk1ID[4];
	uint32_t Subchunk1Size;
	uint16_t AudioFormat;
	uint16_t NumChannels;
	uint32_t SampleRate;
	uint32_t ByteRate;
	uint16_t BlockAlign;
	uint16_t BitsPerSample;
	/* "data" sub-chunk */
	char     Subchunk2ID[4];
	uint32_t Subchunk2Size;
};

struct LoadedSound
{
	char name[RPK_NAME_LENGTH];
	ALuint buffer;
};

#ifndef AUDIO_NO_MUSIC

#define AUDIO_STREAM_NUM_BUFFERS 4
#define AUDIO_STREAM_BUFFER_SIZE 65536
struct AudioStream
{
	ALuint buffers[AUDIO_STREAM_NUM_BUFFERS];
	ALuint source;
	SDL_RWops *file;
	uint8_t channels;
	int32_t sampleRate;
	uint8_t bitsPerSample;
	ALsizei size;
	ALsizei offset;
	ALenum format;
	OggVorbis_File oggFile;
	int oggCurrentSection;
};
bool Stream_Create(struct AudioStream *stream);
bool Stream_LoadFile(struct AudioStream *stream, const char *filename);
void Stream_Update(struct AudioStream *stream);
void Stream_Delete(struct AudioStream *stream);

#endif

static struct LoadedSound sounds[AUDIO_MAX_LOADED];
static int numSounds = 0;

static ALuint sources3d[AUDIO_MAX_3D_SOUNDS];
static ALuint sourcesUI[AUDIO_MAX_UI_SOUNDS];
static int source3dCounter = 0; /* Index of the next audio source to play a sound from */
static int sourceUICounter = 0;

#ifndef AUDIO_NO_MUSIC
static bool musicEnabled = true;
struct AudioStream music;

#define SONG_NAMELEN 32
static char currentSong[SONG_NAMELEN];
#endif

static int LoadWAVFromRPK(const RPKFile *rpk, const char *name, struct WAVHeader *header, void **dataOut, size_t *size)
{
	SDL_RWops *io;
	size_t dataSize, bytesRead;
	void *data;

	dataSize = RPK_GetEntryData(rpk, name, &data);

	io = SDL_RWFromMem(data, dataSize);
	if (io == NULL)
	{
		Msg_Error("Cannot read WAV from entry '%s': %s", name, SDL_GetError());
		SDL_free(data);
		return -1;
	}

	bytesRead = io->read(io, header, sizeof(struct WAVHeader), 1);
	if (bytesRead != 1)
	{
		Msg_Error("Unable to read WAV header from '%s'", name);
		io->close(io);
		SDL_free(data);
		return -1;
	}

	if (SDL_memcmp(header->ChunkID, "RIFF", 4) != 0)
	{
		Msg_Error("'%s' doesn't appear to be a valid WAV file", name);
		io->close(io);
		SDL_free(data);
		return -1;
	}

	*dataOut = SDL_malloc(header->Subchunk2Size);
	io->read(io, *dataOut, 1, header->Subchunk2Size);
	*size = header->Subchunk2Size;

	io->close(io);
	SDL_free(data);
	return 0;
}

void Audio_Init(const RPKFile *rpk, const char *devicename)
{
	ALCdevice *device;
	ALCcontext *context;
	ALenum error;
	int i;

	device = alcOpenDevice(devicename);
	if (device == NULL)
	{
		Msg_Error("Unable to open audio device\n");
		return;
	}

	context = alcCreateContext(device, NULL);
	if (context == NULL)
	{
		Msg_Error("Unable to create audio context\n");
		return;
	}
	alcMakeContextCurrent(context);

	CHECK_AL_ERRORS("Initialization");
	
	Msg_Info("Initialized OpenAL %s from %s", alGetString(AL_VERSION), alGetString(AL_VENDOR));

	alGenSources(AUDIO_MAX_3D_SOUNDS, sources3d);
	alGenSources(AUDIO_MAX_UI_SOUNDS, sourcesUI);
	if ((error = alGetError()) != AL_NO_ERROR)
	{
		Msg_Error("Unable to generate audio sources: %d\n", error);
		return;
	}

	/* Enable 3D effects on 3D sound sources */
	alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
	for (i = 0; i < AUDIO_MAX_3D_SOUNDS; i++)
	{
		ALuint source = sources3d[i];
		alSourcef(source, AL_ROLLOFF_FACTOR, 4.0f);
		alSourcef(source, AL_REFERENCE_DISTANCE, 6.0f);
		alSourcef(source, AL_MAX_DISTANCE, AUDIO_MAX_DISTANCE);
	}
	for (i = 0; i < AUDIO_MAX_UI_SOUNDS; i++)
	{
		ALuint source = sourcesUI[i];
		alSourcei(source, AL_ROLLOFF_FACTOR, 0);
		alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE);
		alSource3i(source, AL_POSITION, 0, 0, 0);
	}

	/* Load all entries that begin with 'Sfx' in the RPK into audio buffers */
	for (i = 0; i < rpk->header->numentries; i++)
	{
		struct WAVHeader wav;
		ALenum format;
		void *data;
		size_t size;

		if (SDL_strncmp(rpk->entries[i].name, "Sfx", 3) != 0)
			continue;

		if (LoadWAVFromRPK(rpk, rpk->entries[i].name, &wav, &data, &size) != 0)
			return;

		if (wav.NumChannels == 1)
			format = wav.BitsPerSample == 8 ? AL_FORMAT_MONO8 : AL_FORMAT_MONO16;
		else
			format = wav.BitsPerSample == 8 ? AL_FORMAT_STEREO8 : AL_FORMAT_STEREO16;

		alGenBuffers(1, &sounds[numSounds].buffer);
		if ((error = alGetError()) != AL_NO_ERROR)
		{
			Msg_Error("Unable to create audio buffer: %d\n", error);
			return;
		}

		alBufferData(sounds[numSounds].buffer, format, data, size, wav.SampleRate);
		if ((error = alGetError()) != AL_NO_ERROR)
		{
			Msg_Error("Unable to load sound '%s' into buffer: %d\n", rpk->entries[i].name, error);
			SDL_free(data);
			continue;
		}

		SDL_memcpy(sounds[numSounds].name, rpk->entries[i].name, RPK_NAME_LENGTH);
		numSounds++;

		/* We don't need the audio data anymore now that it's stored in an OpenAL buffer. */
		SDL_free(data);
	}

	CHECK_AL_ERRORS("Loading sound effects");

#ifndef AUDIO_NO_MUSIC
	Stream_Create(&music);
	SDL_CreateThread(Audio_DoAudioUpdateLoop, "Audio update thread", NULL);
#endif
}

void Audio_Cleanup(void)
{
	ALCdevice *device;
	ALCcontext *context;
	int i;

	for (i = 0; i < numSounds; i++)
	{
		alDeleteBuffers(1, &sounds[i].buffer);
	}

#ifndef AUDIO_NO_MUSIC
	Stream_Delete(&music);
#endif

	alDeleteSources(AUDIO_MAX_3D_SOUNDS, sources3d);
	alDeleteSources(AUDIO_MAX_UI_SOUNDS, sourcesUI);

	context = alcGetCurrentContext();
	device = alcGetContextsDevice(context);

	alcMakeContextCurrent(NULL);
	alcDestroyContext(context);
	alcCloseDevice(device);
}

void Audio_ChangeDevice(const char *deviceName)
{
	ALCdevice *device;
	LPALCREOPENDEVICESOFT func;
	
	device = alcGetContextsDevice(alcGetCurrentContext());

	if (alcIsExtensionPresent(device, "ALC_SOFT_reopen_device") != ALC_TRUE)
	{
		Msg_Error("Audio driver doesn't support changing audio device. Try using OpenAL Soft if you aren't already.");
		return;
	}

	func = (LPALCREOPENDEVICESOFT)alcGetProcAddress(device, "alcReopenDeviceSOFT");
	if (func == NULL)
	{
		Msg_Error("Could not find alcReopenDeviceSOFT function.");
		return;
	}

	func(device, deviceName, NULL);
}

int Audio_GetIDForName(const char *soundname)
{
	int i;
	size_t len = MATH_MIN(RPK_NAME_LENGTH, SDL_strlen(soundname));
	if (len == 1 && *soundname == '0')
		return SOUND_NONE;
	for (i = 0; i < numSounds; i++)
	{
		if (SDL_strncmp(sounds[i].name, soundname, len) == 0)
			return i;
	}
	Msg_Error("Unable to find sound with name '%s'\n", soundname);
	return SOUND_NONE;
}

void Audio_SetVolume(float masterVolume, float sfxVolume, float musicVolume)
{
	int i;

	if (masterVolume < 0.0f) masterVolume = 0.0f;
	if (masterVolume > 1.0f) masterVolume = 1.0f;
	alListenerf(AL_GAIN, masterVolume);

	if (sfxVolume < 0.0f) sfxVolume = 0.0f;
	if (sfxVolume > 1.0f) sfxVolume = 1.0f;
	for (i = 0; i < AUDIO_MAX_3D_SOUNDS; i++)
		alSourcef(sources3d[i], AL_GAIN, sfxVolume);
	for (i = 0; i < AUDIO_MAX_UI_SOUNDS; i++)
		alSourcef(sourcesUI[i], AL_GAIN, sfxVolume);

#ifndef AUDIO_NO_MUSIC
	if (musicVolume < 0.0f) musicVolume = 0.0f;
	if (musicVolume > 1.0f) musicVolume = 1.0f;
	alSourcef(music.source, AL_GAIN, musicVolume);
#endif
}

void Audio_SetMusic(const char *songName)
{
#ifndef AUDIO_NO_MUSIC
	char songPath[SONG_NAMELEN];
	SDL_strlcpy(songPath, "music/", SONG_NAMELEN - 1);
	SDL_strlcat(songPath, songName, SONG_NAMELEN - 1);
	SDL_strlcat(songPath, ".ogg", SONG_NAMELEN - 1);

	if (SDL_strcmp(songPath, currentSong) == 0)
		return;

	alSourceStop(music.source);
	/* TODO: Do this without deleting and recreating the stream */
	Stream_Delete(&music);
	Stream_Create(&music);
	Stream_LoadFile(&music, songPath);
	alSourcePlay(music.source);

	SDL_strlcpy(currentSong, songPath, SONG_NAMELEN - 1);
#endif
}

#ifndef AUDIO_NO_MUSIC
int Audio_DoAudioUpdateLoop(void *data)
{
	(void)data;
	while (1)
	{
		Stream_Update(&music);
		SDL_Delay(100);
	}
}
#endif

void Audio_UpdateListener(void)
{
	/* Update listener position and orientation */
	ALfloat orientation[] = {0.0f, 0.0f, 0.0f,	0.0f, 1.0f, 0.0f};

	orientation[0] = Math_Sin(ANGLE2FLOAT(World_Players[0].sprite->angle));
	orientation[2] = Math_Cos(ANGLE2FLOAT(World_Players[0].sprite->angle));

	alListener3f(AL_POSITION, TOFLOAT(World_Players[0].sprite->x), 0.0f, TOFLOAT(World_Players[0].sprite->y));
	// TODO: Don't have client's movement speed to do doppler shift.
	//		It's so hard to notice, that I might just remove it entirely.
	// alListener3f(AL_VELOCITY, World_Players[0].entity->xd, 0.0f, World_Players[0].entity->yd);
	alListenerfv(AL_ORIENTATION, orientation);
}

void Audio_PlaySound(int id)
{
	ALint sourceId = sourcesUI[sourceUICounter];
	if (id > numSounds)
	{
		Msg_Warning("Attempted to play invalid sound id %d", id);
		return;
	}

	sourceUICounter = (sourceUICounter + 1) % AUDIO_MAX_UI_SOUNDS;
	alSourceStop(sourceId);
	alSourcei(sourceId, AL_BUFFER, sounds[id].buffer);
	alSourcePlay(sourceId);
}

void Audio_PlaySound3D(int id, float srcx, float srcy, float velx, float vely)
{
	float xx = TOFLOAT(World_Players[0].sprite->x) - srcx;
	float yy = TOFLOAT(World_Players[0].sprite->y) - srcy;

	if (id > numSounds)
	{
		Msg_Warning("Attempted to play invalid sound id %d", id);
		return;
	}

	if (xx * xx + yy * yy <= AUDIO_MAX_DISTANCE * AUDIO_MAX_DISTANCE)
	{
		ALint sourceId = sources3d[source3dCounter];
		source3dCounter = (source3dCounter + 1) % AUDIO_MAX_3D_SOUNDS;

		alSourceStop(sourceId);
		alSource3f(sourceId, AL_POSITION, srcx, 0.0f, srcy);
		alSource3f(sourceId, AL_VELOCITY, velx, 0.0f, vely);
		alSourcei(sourceId, AL_BUFFER, sounds[id].buffer);
		alSourcePlay(sourceId);
	}
}

int Audio_GetDeviceList(char **devicelist)
{
	ALCchar *ptr, *next;
	int numDevices = 0;
	const char *devices;

	if (alcIsExtensionPresent(NULL, "ALC_enumeration_EXT") == AL_FALSE)
	{
		devicelist[0] = "Default Device (can't enumerate)";
		return 1;
	}

	if (alcIsExtensionPresent(NULL, "ALC_enumerate_all_EXT") == AL_TRUE)
		devices = alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER);
	else
		devices = alcGetString(NULL, ALC_DEVICE_SPECIFIER);

	/* Check how many devices there are so we know how many elements to allocate in the string array */
	ptr = devices;
	next = ptr;
	do
	{
		ptr = next;
		if (numDevices == AUDIO_MAXDEVICES)
			break;
		devicelist[numDevices] = ptr;
		numDevices++;
	} while (*(next += SDL_strlen(ptr) + 1) != 0);
	
	return numDevices;
}

#ifndef AUDIO_NO_MUSIC
static size_t ReadOGGCallback(void *dst, size_t size, size_t nmemb, void *streamPtr)
{
	struct AudioStream *stream = (struct AudioStream *)streamPtr;
	char *moreData;

	ALsizei length = size * nmemb;

	if (stream->offset + length > stream->size)
		length = stream->size - stream->offset;

	if (!stream->file)
	{
		Msg_Error("Lost access to streaming audio file");
		return 0;
	}

	moreData = SDL_malloc(length);

	stream->file->seek(stream->file, stream->offset, RW_SEEK_SET);
	if (!stream->file->read(stream->file, moreData, length, 1))
	{
		/* Check error somehow??? */
	}
	
	stream->offset += length;
	SDL_memcpy(dst, moreData, length);
	SDL_free(moreData);

	return length;
}

static int SeekOGGCallback(void *streamPtr, ogg_int64_t offset, int32_t whence)
{
	struct AudioStream *stream = (struct AudioStream *)streamPtr;
	switch (whence)
	{
	case SEEK_CUR:
		stream->offset += offset;
		break;
	case SEEK_END:
		stream->offset = stream->size - offset;
		break;
	case SEEK_SET:
		stream->offset = offset;
		break;
	default:
		return -1;
	}
	if (stream->offset < 0)
	{
		stream->offset = 0;
		return -1;
	}
	if (stream->offset > stream->size)
	{
		stream->offset = stream->size;
		return -1;
	}
	return 0;
}

static long TellOGGCallback(void *streamPtr)
{
	struct AudioStream *stream = (struct AudioStream *)streamPtr;
	return stream->offset;
}

bool Stream_Create(struct AudioStream *stream)
{
	CLEAR_AL_ERRORS();

	SDL_memset(stream, 0, sizeof(struct AudioStream));

	alGenSources(1, &stream->source);
	alSourcef(stream->source, AL_GAIN, Game.options.musicVolume);
	alSourcef(stream->source, AL_PITCH, 1);
	alSource3f(stream->source, AL_POSITION, 0, 0, 0);
	alSource3f(stream->source, AL_VELOCITY, 0, 0, 0);
	alGenBuffers(AUDIO_STREAM_NUM_BUFFERS, stream->buffers);

	CHECK_AL_ERRORS("Creating audio stream");

	return true;
}

bool Stream_LoadFile(struct AudioStream *stream, const char *filename)
{
	int bruh;

	alSourceStop(stream->source);

	if (stream->file)
		stream->file->close(stream->file);

	stream->file = SDL_RWFromFile(filename, "rb");
	if (!stream->file)
	{
		Msg_Error("Unable to open %s for streaming: %s", filename, SDL_GetError());
		return false;
	}

	stream->size = SDL_RWsize(stream->file);
	stream->offset = 0;

	ov_callbacks oggCallbacks;
	oggCallbacks.read_func = ReadOGGCallback;
	oggCallbacks.close_func = NULL;
	oggCallbacks.seek_func = SeekOGGCallback;
	oggCallbacks.tell_func = TellOGGCallback;

	if ((bruh = ov_open_callbacks(stream, &stream->oggFile, NULL, -1, oggCallbacks)) < 0)
	{
		char *reason;
		switch (bruh)
		{
		case OV_EREAD:
			reason = "Unable to read file";
			break;
		case OV_ENOTVORBIS:
			reason = "Not an ogg vorbis file";
			break;
		case OV_EVERSION:
			reason = "Incorrect vorbis version";
			break;
		case OV_EBADHEADER:
			reason = "Bad vorbis header";
			break;
		case OV_EFAULT:
			reason = "Internal logic fault";
			break;
		default:
			reason = "Unknown reason";
			break;
		}
		Msg_Error("Failed to open '%s': %s (%d)", filename, reason, bruh);
		return false;
	}

	vorbis_info *vorbisInfo = ov_info(&stream->oggFile, -1);
	stream->channels = vorbisInfo->channels;
	stream->bitsPerSample = 16;
	stream->sampleRate = vorbisInfo->rate;
	if (stream->channels == 1 && stream->bitsPerSample == 8)
		stream->format = AL_FORMAT_MONO8;
	else if (stream->channels == 1 && stream->bitsPerSample == 16)
		stream->format = AL_FORMAT_MONO16;
	else if (stream->channels == 2 && stream->bitsPerSample == 8)
		stream->format = AL_FORMAT_STEREO8;
	else if (stream->channels == 2 && stream->bitsPerSample == 16)
		stream->format = AL_FORMAT_STEREO16;
	else
	{
		Msg_Error("Stream from %s has unrecognized format: %d-bit, %d channels", filename, stream->bitsPerSample, stream->channels);
		return false;
	}

	CLEAR_AL_ERRORS();

	char *data = SDL_malloc(AUDIO_STREAM_BUFFER_SIZE);

	for (int i = 0; i < AUDIO_STREAM_NUM_BUFFERS; i++)
	{
		int dataSoFar = 0;
		while (dataSoFar < AUDIO_STREAM_BUFFER_SIZE)
		{
			long result = ov_read(&stream->oggFile, &data[dataSoFar], AUDIO_STREAM_BUFFER_SIZE - dataSoFar, 0, 2, 1, &stream->oggCurrentSection);
			if (result == OV_HOLE)
			{
				Msg_Error("OV_HOLE in initial read of audio stream from %s", filename);
				break;
			}
			else if (result == OV_EBADLINK)
			{
				Msg_Error("OV_EBADLINK in initial read of audio stream from %s", filename);
				break;
			}
			else if (result == OV_EINVAL)
			{
				Msg_Error("OV_EINVAL in initial read of audio stream from %s", filename);
				break;
			}
			else if (result == 0)
			{
				Msg_Error("End-of-file in initial read of audio stream from %s", filename);
				break;
			}
			dataSoFar += result;
		}

		alBufferData(stream->buffers[i], stream->format, data, dataSoFar, stream->sampleRate);
	}
	alSourceQueueBuffers(stream->source, AUDIO_STREAM_NUM_BUFFERS, stream->buffers);
	CHECK_AL_ERRORS("Initial read of file into stream");

	SDL_free(data);

	return true;
}

void Stream_Delete(struct AudioStream *stream)
{
	CLEAR_AL_ERRORS();
	if (stream->file)
		stream->file->close(stream->file);
	alDeleteBuffers(AUDIO_STREAM_NUM_BUFFERS, stream->buffers);
	/* TODO: Don't just ignore the error that is created here. */
	alDeleteSources(1, &stream->source);
	CLEAR_AL_ERRORS();
}

void Stream_Update(struct AudioStream *stream)
{
	CLEAR_AL_ERRORS();
	ALint buffersProcessed = 0;
	alGetSourcei(stream->source, AL_BUFFERS_PROCESSED, &buffersProcessed);
	if (buffersProcessed <= 0)
		return;
	while (buffersProcessed--)
	{
		ALuint buffer;
		ALsizei dataSizeToBuffer = 0;
		int sizeRead = 0;
		char *data;

		alSourceUnqueueBuffers(stream->source, 1, &buffer);

		data = SDL_malloc(AUDIO_STREAM_BUFFER_SIZE);
		SDL_memset(data, 0, AUDIO_STREAM_BUFFER_SIZE);

		while (sizeRead < AUDIO_STREAM_BUFFER_SIZE)
		{
			long result = ov_read(&stream->oggFile, &data[sizeRead], AUDIO_STREAM_BUFFER_SIZE - sizeRead, 0, 2, 1, &stream->oggCurrentSection);
			if (result < 0)
			{
				Msg_Error("Failed to update audio stream");
				break;
			}
			else if (result == 0)
			{
				/* Seek to beginning of file if we've reached the end */
				if (ov_raw_seek(&stream->oggFile, 0) != 0)
				{
					return;
				}
			}
			sizeRead += result;
		}
		dataSizeToBuffer = sizeRead;

		if (dataSizeToBuffer > 0)
		{
			alBufferData(buffer, stream->format, data, dataSizeToBuffer, stream->sampleRate);
			alSourceQueueBuffers(stream->source, 1, &buffer);
		}
		if (dataSizeToBuffer < AUDIO_STREAM_BUFFER_SIZE)
			Msg_Error("Data missing");
		ALint state;
		alGetSourcei(stream->source, AL_SOURCE_STATE, &state);
		if (state != AL_PLAYING)
		{
			alSourceStop(stream->source);
			alSourcePlay(stream->source);
		}
		SDL_free(data);
	}
	CHECK_AL_ERRORS("Updating audio stream");
}
#endif /* AUDIO_NO_MUSIC */
