/*
** soundemitter.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2014 Jonas Kulla <Nyocurio@gmail.com>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "soundemitter.h"

#include "sharedstate.h"
#include "filesystem.h"
#include "exception.h"
#include "config.h"
#include "util.h"
#include "debugwriter.h"

#include <SDL_sound.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/fetch.h>
#include <string>
#include <fstream>
#endif

#define SE_CACHE_MEM (10*1024*1024) // 10 MB

struct SoundBuffer
{
	/* Uniquely identifies this or equal buffer */
	std::string key;

	AL::Buffer::ID alBuffer;

	/* Link into the buffer cache priority list */
	IntruListLink<SoundBuffer> link;

	/* Buffer byte count */
	uint32_t bytes;

	/* Reference count */
	uint8_t refCount;

	SoundBuffer()
	    : link(this),
	      refCount(1)

	{
		alBuffer = AL::Buffer::gen();
	}

	static SoundBuffer *ref(SoundBuffer *buffer)
	{
		++buffer->refCount;

		return buffer;
	}

	static void deref(SoundBuffer *buffer)
	{
		if (--buffer->refCount == 0)
			delete buffer;
	}

private:
	~SoundBuffer()
	{
		AL::Buffer::del(alBuffer);
	}
};

/* Before: [a][b][c][d], After (index=1): [a][c][d][b] */
static void
arrayPushBack(std::vector<size_t> &array, size_t size, size_t index)
{
	size_t v = array[index];

	for (size_t t = index; t < size-1; ++t)
		array[t] = array[t+1];

	array[size-1] = v;
}

SoundEmitter::SoundEmitter(const Config &conf)
    : bufferBytes(0),
      srcCount(conf.SE.sourceCount),
      alSrcs(srcCount),
      atchBufs(srcCount),
      srcPrio(srcCount)
{
	for (size_t i = 0; i < srcCount; ++i)
	{
		alSrcs[i] = AL::Source::gen();
		atchBufs[i] = 0;
		srcPrio[i] = i;
	}
}

SoundEmitter::~SoundEmitter()
{
	for (size_t i = 0; i < srcCount; ++i)
	{
		AL::Source::stop(alSrcs[i]);
		AL::Source::del(alSrcs[i]);

		if (atchBufs[i])
			SoundBuffer::deref(atchBufs[i]);
	}

	BufferHash::const_iterator iter;
	for (iter = bufferHash.cbegin(); iter != bufferHash.cend(); ++iter)
		SoundBuffer::deref(iter->second);
}

struct PlayInternalCallbackData {
	SoundEmitter * se;
	int volume;
	int pitch;
};

void SoundEmitter::play_internal(const std::string &filename,
                        int volume,
                        int pitch,
			emscripten_fetch_t * fetch)
{
	float _volume = clamp<int>(volume, 0, 100) / 100.0f;
	float _pitch  = clamp<int>(pitch, 50, 150) / 100.0f;

	SoundBuffer *buffer = allocateBuffer(filename, fetch);

	if (!buffer)
		return;

	/* Try to find first free source */
	size_t i;
	for (i = 0; i < srcCount; ++i)
		if (AL::Source::getState(alSrcs[srcPrio[i]]) != AL_PLAYING)
			break;

	/* If we didn't find any, try to find the lowest priority source
	 * with the same buffer to overtake */
	if (i == srcCount)
		for (size_t j = 0; j < srcCount; ++j)
			if (atchBufs[srcPrio[j]] == buffer)
				i = j;

	/* If we didn't find any, overtake the one with lowest priority */
	if (i == srcCount)
		i = 0;

	size_t srcIndex = srcPrio[i];

	/* Only detach/reattach if it's actually a different buffer */
	bool switchBuffer = (atchBufs[srcIndex] != buffer);

	/* Push the used source to the back of the priority list */
	arrayPushBack(srcPrio, srcCount, i);

	AL::Source::ID src = alSrcs[srcIndex];
	AL::Source::stop(src);

	if (switchBuffer)
		AL::Source::detachBuffer(src);

	SoundBuffer *old = atchBufs[srcIndex];

	if (old)
		SoundBuffer::deref(old);

	atchBufs[srcIndex] = SoundBuffer::ref(buffer);

	if (switchBuffer)
		AL::Source::attachBuffer(src, buffer->alBuffer);

	AL::Source::setVolume(src, _volume * GLOBAL_VOLUME);
	AL::Source::setPitch(src, _pitch);

	AL::Source::play(src);
}

void saveAudioFile(void * arg) {
	emscripten_fetch_t * fetch = (emscripten_fetch_t *) arg;
		/*std::string fname(fetch->url);
		for (size_t i = 0; i < fname.size(); i++) {
			if (fname[i] == '/') fname[i] = '_';
		}
		fname = fname + ".ogg";*/
	try {
		PlayInternalCallbackData * data = ((PlayInternalCallbackData *) fetch->userData);
		data->se->play_internal(fetch->url, data->volume, data->pitch, fetch);
	} catch (const Exception &e) {}
	delete ((PlayInternalCallbackData *) fetch->userData);
	emscripten_fetch_close(fetch);
}

void audioDownloadSucceeded(emscripten_fetch_t *fetch) {
	emscripten_push_main_loop_blocker(saveAudioFile, (void *) fetch);
}

void audioDownloadFailed(emscripten_fetch_t *fetch) {
	printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
	delete ((PlayInternalCallbackData *) fetch->userData);
	emscripten_fetch_close(fetch);
}

void addAudioDownload(const char * filename, PlayInternalCallbackData * se) {
	const char *pfx = "async/";
	char result[512];
	strcpy(result, pfx);
	strcat(result, filename);

	emscripten_fetch_attr_t attr;
	emscripten_fetch_attr_init(&attr);
	strcpy(attr.requestMethod, "GET");
	attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
	attr.onsuccess = audioDownloadSucceeded;
	attr.onerror = audioDownloadFailed;
	attr.userData = se;
	emscripten_fetch(&attr, result);
}

void SoundEmitter::play(const std::string &filename,
                        int volume,
                        int pitch) {
	PlayInternalCallbackData * data = new PlayInternalCallbackData();
	data->se = this;
	data->volume = volume;
	data->pitch = pitch;
	addAudioDownload(filename.c_str(), data);
}

void SoundEmitter::stop()
{
	for (size_t i = 0; i < srcCount; i++)
		AL::Source::stop(alSrcs[i]);
}

struct SoundOpenHandler : FileSystem::OpenHandler
{
	SoundBuffer *buffer;

	SoundOpenHandler()
	    : buffer(0)
	{}

	void decode(Sound_Sample *sample) {
		/* Do all of the decoding in the handler so we don't have
		 * to keep the source ops around */
		uint32_t decBytes = Sound_DecodeAll(sample);
		uint8_t sampleSize = formatSampleSize(sample->actual.format);
		uint32_t sampleCount = decBytes / sampleSize;

		buffer = new SoundBuffer;
		buffer->bytes = sampleSize * sampleCount;

		ALenum alFormat = chooseALFormat(sampleSize, sample->actual.channels);

		AL::Buffer::uploadData(buffer->alBuffer, alFormat, sample->buffer,
							   buffer->bytes, sample->actual.rate);

		Sound_FreeSample(sample);
	}

	bool tryRead(SDL_RWops &ops, const char *ext)
	{
		Sound_Sample *sample = Sound_NewSample(&ops, ext, 0, STREAM_BUF_SIZE);

		if (!sample)
		{
			SDL_RWclose(&ops);
			return false;
		}

		decode(sample);

		return true;
	}

	bool emread(emscripten_fetch_t *fetch) {
		Sound_Sample *sample = Sound_NewSample(SDL_RWFromConstMem(fetch->data, fetch->numBytes), "ogg", 0, STREAM_BUF_SIZE);

		if (!sample)
		{
			return false;
		}

		decode(sample);

		return true;
	}
};

SoundBuffer *SoundEmitter::allocateBuffer(const std::string &filename, emscripten_fetch_t *fetch)
{
	SoundBuffer *buffer = bufferHash.value(filename, 0);

	if (buffer)
	{
		/* Buffer still in cashe.
		 * Move to front of priority list */
		buffers.remove(buffer->link);
		buffers.append(buffer->link);

		return buffer;
	}
	else
	{
		/* Buffer not in cache, needs to be loaded */
		SoundOpenHandler handler;
		handler.emread(fetch);
		buffer = handler.buffer;

		if (!buffer)
		{
			char buf[512];
			snprintf(buf, sizeof(buf), "Unable to decode sound: %s: %s",
			         filename.c_str(), Sound_GetError());
			Debug() << buf;

			return 0;
		}

		buffer->key = filename;
		uint32_t wouldBeBytes = bufferBytes + buffer->bytes;

		/* If memory limit is reached, delete lowest priority buffer
		 * until there is room or no buffers left */
		while (wouldBeBytes > SE_CACHE_MEM && !buffers.isEmpty())
		{
			SoundBuffer *last = buffers.tail();
			bufferHash.remove(last->key);
			buffers.remove(last->link);

			wouldBeBytes -= last->bytes;

			SoundBuffer::deref(last);
		}

		bufferHash.insert(filename, buffer);
		buffers.prepend(buffer->link);

		bufferBytes = wouldBeBytes;

		return buffer;
	}
}