/**
* @file streamingaudio_fmodstudio.cpp
* @brief LLStreamingAudio_FMODSTUDIO implementation
*
* $LicenseInfo:firstyear=2002&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2010, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
* $/LicenseInfo$
*/

#include "linden_common.h"
#include "llstreamingaudio_fmodstudio.h"

#include "llsd.h"
#include "llmath.h"
#include "llmutex.h"

#include "fmod.hpp"
#include "fmod_errors.h"

inline bool Check_FMOD_Stream_Error(FMOD_RESULT result, const char *string)
{
	if (result == FMOD_OK)
		return false;
	LL_WARNS("AudioImpl") << string << " Error: " << FMOD_ErrorString(result) << LL_ENDL;
	return true;
}

class LLAudioStreamManagerFMODSTUDIO
{
public:
	LLAudioStreamManagerFMODSTUDIO(FMOD::System *system, FMOD::ChannelGroup *group, const std::string& url);
	FMOD::Channel* startStream();
	bool stopStream(); // Returns true if the stream was successfully stopped.

	const std::string& getURL() 	{ return mInternetStreamURL; }

	FMOD_RESULT getOpenState(FMOD_OPENSTATE& openstate, unsigned int* percentbuffered = nullptr, bool* starving = nullptr, bool* diskbusy = nullptr);
protected:
	FMOD::System* mSystem;
	FMOD::ChannelGroup* mChannelGroup;
	FMOD::Channel* mStreamChannel;
	FMOD::Sound* mInternetStream;
	bool mReady;

	std::string mInternetStreamURL;
};

//BD - We don't have C++11 yet.
/*std::recursive_mutex gWaveDataMutex;	//Just to be extra strict.
const U32 WAVE_BUFFER_SIZE = 1024;
U32 gWaveBufferMinSize = 0;
F32 gWaveDataBuffer[WAVE_BUFFER_SIZE] = { 0.f };
U32 gWaveDataBufferSize = 0;

FMOD_RESULT F_CALLBACK waveDataCallback(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels)
{
	if (!length || !inchannels)
		return FMOD_OK;
	memcpy(outbuffer, inbuffer, length * inchannels * sizeof(float));

	static std::vector<F32> local_buf;
	if (local_buf.size() < length)
		local_buf.resize(length, 0.f);

	for (U32 i = 0; i < length; ++i)
	{
		F32 total = 0.f;
		for (S32 j = 0; j < inchannels; ++j)
		{
			total += inbuffer[i*inchannels + j];
		}
		local_buf[i] = total / inchannels;
	}

	{
		gWaveDataMutex.lock();

		for (U32 i = length; i > 0; --i)
		{
			if (++gWaveDataBufferSize > WAVE_BUFFER_SIZE)
			{
				if (gWaveBufferMinSize)
					memcpy(gWaveDataBuffer + WAVE_BUFFER_SIZE - gWaveBufferMinSize, gWaveDataBuffer, gWaveBufferMinSize * sizeof(float));
				gWaveDataBufferSize = 1 + gWaveBufferMinSize;
			}
			gWaveDataBuffer[WAVE_BUFFER_SIZE - gWaveDataBufferSize] = local_buf[i - 1];
		}
		gWaveDataMutex.unlock();
	}

	return FMOD_OK;
}*/

//---------------------------------------------------------------------------
// Internet Streaming
//---------------------------------------------------------------------------
LLStreamingAudio_FMODSTUDIO::LLStreamingAudio_FMODSTUDIO(FMOD::System *system) :
mSystem(system),
mCurrentInternetStreamp(nullptr),
mStreamDSP(nullptr),
mStreamGroup(nullptr),
mFMODInternetStreamChannelp(nullptr),
mGain(1.0f),
mMetaData(nullptr),
mNewMetadata(true)
{
	FMOD_RESULT result;

	// Number of milliseconds of audio to buffer for the audio card.
	// Must be larger than the usual Second Life frame stutter time.
	const U32 buffer_seconds = 10;		//sec
	const U32 estimated_bitrate = 128;	//kbit/sec
	result = mSystem->setStreamBufferSize(estimated_bitrate * buffer_seconds * 128/*bytes/kbit*/, FMOD_TIMEUNIT_RAWBYTES);
	Check_FMOD_Stream_Error(result, "FMOD::System::setStreamBufferSize");

	Check_FMOD_Stream_Error(system->createChannelGroup("stream", &mStreamGroup), "FMOD::System::createChannelGroup");

	//BD - We don't have C++11 yet.
	/*FMOD_DSP_DESCRIPTION dspdesc = {};
	dspdesc.pluginsdkversion = FMOD_PLUGIN_SDK_VERSION;
	strncpy(dspdesc.name, "Waveform", sizeof(dspdesc.name));
	dspdesc.numoutputbuffers = 1;
	dspdesc.read = &waveDataCallback; //Assign callback.

	Check_FMOD_Stream_Error(system->createDSP(&dspdesc, &mStreamDSP), "FMOD::System::createDSP");*/
}

LLStreamingAudio_FMODSTUDIO::~LLStreamingAudio_FMODSTUDIO()
{
	stop();
	for (U32 i = 0; i < 100; ++i)
	{
		if (releaseDeadStreams())
			break;
		ms_sleep(10);
	}

	cleanupWaveData();
}

void LLStreamingAudio_FMODSTUDIO::start(const std::string& url)
{
	//if (!mInited)
	//{
	//	LL_WARNS() << "startInternetStream before audio initialized" << LL_ENDL;
	//	return;
	//}

	// "stop" stream but don't clear url, etc. in case url == mInternetStreamURL
	stop();

	if (!url.empty())
	{
		if (mDeadStreams.empty())
		{
			LL_INFOS() << "Starting internet stream: " << url << LL_ENDL;
			mCurrentInternetStreamp = new LLAudioStreamManagerFMODSTUDIO(mSystem, mStreamGroup, url);
			mURL = url;
			mMetaData = new LLSD;
		}
		else
		{
			LL_INFOS() << "Deferring stream load until buffer release: " << url << LL_ENDL;
			mPendingURL = url;
		}
	}
	else
	{
		LL_INFOS() << "Set internet stream to null" << LL_ENDL;
		mURL.clear();
	}
}

enum utf_endian_type_t
{
	UTF16LE,
	UTF16BE,
	UTF16
};

std::string utf16input_to_utf8(unsigned char* input, U32 len, utf_endian_type_t type)
{
	if (type == UTF16)
	{
		type = UTF16BE;	//Default
		if (len > 2)
		{
			//Parse and strip BOM.
			if ((input[0] == 0xFE && input[1] == 0xFF) ||
				(input[0] == 0xFF && input[1] == 0xFE))
			{
				input += 2;
				len -= 2;
				type = input[0] == 0xFE ? UTF16BE : UTF16LE;
			}
		}
	}
	llutf16string out_16((llutf16string::value_type*)input, len / 2);
	if (len % 2)
	{
		out_16.push_back((input)[len - 1] << 8);
	}
	if (type == UTF16BE)
	{
		for (llutf16string::iterator i = out_16.begin(); i < out_16.end(); ++i)
		{
			llutf16string::value_type v = *i;
			*i = ((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8);
		}
	}
	return utf16str_to_utf8str(out_16);
}

void LLStreamingAudio_FMODSTUDIO::update()
{
	if (!releaseDeadStreams())
	{
		llassert_always(mCurrentInternetStreamp == NULL);
		return;
	}

	if (!mPendingURL.empty())
	{
		llassert_always(mCurrentInternetStreamp == NULL);
		LL_INFOS() << "Starting internet stream: " << mPendingURL << LL_ENDL;
		mCurrentInternetStreamp = new LLAudioStreamManagerFMODSTUDIO(mSystem, mStreamGroup, mPendingURL);
		mURL = mPendingURL;
		mMetaData = new LLSD;
		mPendingURL.clear();
	}

	// Don't do anything if there are no streams playing
	if (!mCurrentInternetStreamp)
	{
		return;
	}

	unsigned int progress;
	bool starving;
	bool diskbusy;
	FMOD_OPENSTATE open_state;
	FMOD_RESULT result = mCurrentInternetStreamp->getOpenState(open_state, &progress, &starving, &diskbusy);

	if (result != FMOD_OK || open_state == FMOD_OPENSTATE_ERROR)
	{
		stop();
		return;
	}
	else if (open_state == FMOD_OPENSTATE_READY)
	{
		// Stream is live

		// start the stream if it's ready
		if (!mFMODInternetStreamChannelp &&
			(mFMODInternetStreamChannelp = mCurrentInternetStreamp->startStream()))
		{
			// Reset volume to previously set volume
			setGain(getGain());
			if (mStreamDSP)
			{
				Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->addDSP(FMOD_CHANNELCONTROL_DSP_TAIL, mStreamDSP), "FMOD::Channel::addDSP");
			}
			Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setPaused(false), "FMOD::Channel::setPaused");
		}
	}


	if (mFMODInternetStreamChannelp)
	{
		if (!mMetaData)
			mMetaData = new LLSD;

		FMOD::Sound *sound = nullptr;

		if (mFMODInternetStreamChannelp->getCurrentSound(&sound) == FMOD_OK && sound)
		{
			FMOD_TAG tag;
			S32 tagcount, dirtytagcount;
			if (sound->getNumTags(&tagcount, &dirtytagcount) == FMOD_OK && dirtytagcount)
			{
				mMetaData->clear();
				mNewMetadata = true;

				for (S32 i = 0; i < tagcount; ++i)
				{
					if (sound->getTag(nullptr, i, &tag) != FMOD_OK)
						continue;

					std::string name = tag.name;
					switch (tag.type)	//Crappy tag translate table.
					{
					case(FMOD_TAGTYPE_ID3V2) :
						if (!LLStringUtil::compareInsensitive(name, "TIT2")) name = "TITLE";
						else if (name == "TPE1") name = "ARTIST";
						break;
					case(FMOD_TAGTYPE_ASF) :
						if (!LLStringUtil::compareInsensitive(name, "Title")) name = "TITLE";
						else if (!LLStringUtil::compareInsensitive(name, "WM/AlbumArtist")) name = "ARTIST";
						break;
					case(FMOD_TAGTYPE_FMOD) :
						if (!LLStringUtil::compareInsensitive(name, "Sample Rate Change"))
						{
							LL_INFOS() << "Stream forced changing sample rate to " << *((float *)tag.data) << LL_ENDL;
							Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setFrequency(*((float *)tag.data)), "FMOD::Channel::setFrequency");
						}
											continue;
					default:
						if (!LLStringUtil::compareInsensitive(name, "TITLE") ||
							!LLStringUtil::compareInsensitive(name, "ARTIST"))
							LLStringUtil::toUpper(name);
						break;
					}

					switch (tag.datatype)
					{
					case(FMOD_TAGDATATYPE_INT) :
						(*mMetaData)[name] = *(LLSD::Integer*)(tag.data);
						LL_INFOS() << tag.name << ": " << *(int*)(tag.data) << LL_ENDL;
						break;
					case(FMOD_TAGDATATYPE_FLOAT) :
						(*mMetaData)[name] = *(LLSD::Real*)(tag.data);
						LL_INFOS() << tag.name << ": " << *(float*)(tag.data) << LL_ENDL;
						break;
					case(FMOD_TAGDATATYPE_STRING) :
					{
						std::string out = rawstr_to_utf8(std::string((char*)tag.data, tag.datalen));
						if (out.length() && out[out.size() - 1] == 0)
							out.erase(out.size() - 1);
						(*mMetaData)[name] = out;
						LL_INFOS() << tag.name << "(RAW): " << out << LL_ENDL;
					}
												  break;
					case(FMOD_TAGDATATYPE_STRING_UTF8) :
					{
						U8 offs = 0;
						if (tag.datalen > 3 && ((unsigned char*)tag.data)[0] == 0xEF && ((unsigned char*)tag.data)[1] == 0xBB && ((unsigned char*)tag.data)[2] == 0xBF)
							offs = 3;
						std::string out((char*)tag.data + offs, tag.datalen - offs);
						if (out.length() && out[out.size() - 1] == 0)
							out.erase(out.size() - 1);
						(*mMetaData)[name] = out;
						LL_INFOS() << tag.name << "(UTF8): " << out << LL_ENDL;
					}
													   break;
					case(FMOD_TAGDATATYPE_STRING_UTF16) :
					{
						std::string out = utf16input_to_utf8((unsigned char*)tag.data, tag.datalen, UTF16);
						if (out.length() && out[out.size() - 1] == 0)
							out.erase(out.size() - 1);
						(*mMetaData)[name] = out;
						LL_INFOS() << tag.name << "(UTF16): " << out << LL_ENDL;
					}
														break;
					case(FMOD_TAGDATATYPE_STRING_UTF16BE) :
					{
						std::string out = utf16input_to_utf8((unsigned char*)tag.data, tag.datalen, UTF16BE);
						if (out.length() && out[out.size() - 1] == 0)
							out.erase(out.size() - 1);
						(*mMetaData)[name] = out;
						LL_INFOS() << tag.name << "(UTF16BE): " << out << LL_ENDL;
					}
					default:
						break;
					}
				}
			}
			static bool was_starved = false;
			if (starving)
			{
				bool paused = false;
				if (mFMODInternetStreamChannelp->getPaused(&paused) == FMOD_OK && !paused)
				{
					LL_INFOS() << "Stream starvation detected! Pausing stream until buffer nearly full." << LL_ENDL;
					LL_INFOS() << "  (diskbusy=" << diskbusy << ")" << LL_ENDL;
					LL_INFOS() << "  (progress=" << progress << ")" << LL_ENDL;
					Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setPaused(true), "FMOD::Channel::setPaused");
				}
				was_starved = true;
			}
			else if (progress > 80 && was_starved)
			{
				was_starved = false;
				Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setPaused(false), "FMOD::Channel::setPaused");
			}
		}
	}
}

void LLStreamingAudio_FMODSTUDIO::stop()
{
	mPendingURL.clear();

	if (mMetaData)
	{
		delete mMetaData;
		mMetaData = nullptr;
	}

	if (mFMODInternetStreamChannelp)
	{
		Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setPaused(true), "FMOD::Channel::setPaused");
		Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setPriority(0), "FMOD::Channel::setPriority");
		if (mStreamDSP)
		{
			Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->removeDSP(mStreamDSP), "FMOD::Channel::removeDSP");
		}
		mFMODInternetStreamChannelp = nullptr;
	}

	if (mCurrentInternetStreamp)
	{
		LL_INFOS() << "Stopping internet stream: " << mCurrentInternetStreamp->getURL() << LL_ENDL;
		if (mCurrentInternetStreamp->stopStream())
		{
			delete mCurrentInternetStreamp;
		}
		else
		{
			LL_WARNS() << "Pushing stream to dead list: " << mCurrentInternetStreamp->getURL() << LL_ENDL;
			mDeadStreams.push_back(mCurrentInternetStreamp);
		}
		mCurrentInternetStreamp = nullptr;
	}
}

void LLStreamingAudio_FMODSTUDIO::pause(S32 pauseopt)
{
	if (pauseopt < 0)
	{
		pauseopt = mCurrentInternetStreamp ? 1 : 0;
	}

	if (pauseopt)
	{
		if (mCurrentInternetStreamp)
		{
			stop();
		}
	}
	else
	{
		start(getURL());
	}
}


// A stream is "playing" if it has been requested to start.  That
// doesn't necessarily mean audio is coming out of the speakers.
int LLStreamingAudio_FMODSTUDIO::isPlaying()
{
	if (mCurrentInternetStreamp)
	{
		return 1; // Active and playing
	}
	else if (!mURL.empty() || !mPendingURL.empty())
	{
		return 2; // "Paused"
	}
	else
	{
		return 0;
	}
}


F32 LLStreamingAudio_FMODSTUDIO::getGain()
{
	return mGain;
}


std::string LLStreamingAudio_FMODSTUDIO::getURL()
{
	return mURL;
}


void LLStreamingAudio_FMODSTUDIO::setGain(F32 vol)
{
	mGain = vol;

	if (mFMODInternetStreamChannelp)
	{
		vol = llclamp(vol * vol, 0.f, 1.f);	//should vol be squared here?

		Check_FMOD_Stream_Error(mFMODInternetStreamChannelp->setVolume(vol), "FMOD::Channel::setVolume");
	}
}

const bool LLStreamingAudio_FMODSTUDIO::hasNewMetaData()
{
	if (mCurrentInternetStreamp && mNewMetadata)
	{
		mNewMetadata = false;
		return true;
	}
	else
	{
		return false;
	}
}

//BD - We don't have C++11 yet.
/* virtual */
/*bool LLStreamingAudio_FMODSTUDIO::getWaveData(float* arr, S32 count, S32 stride)
{
	if (count > (WAVE_BUFFER_SIZE / 2))
		LL_ERRS("AudioImpl") << "Count=" << count << " exceeds WAVE_BUFFER_SIZE/2=" << WAVE_BUFFER_SIZE << LL_ENDL;

	if (!mFMODInternetStreamChannelp || !mCurrentInternetStreamp)
		return false;

	bool muted = false;
	FMOD_RESULT res = mFMODInternetStreamChannelp->getMute(&muted);
	if (res != FMOD_OK || muted)
		return false;
	{
		U32 buff_size;
		{
			gWaveDataMutex.lock();
			gWaveBufferMinSize = count;
			buff_size = gWaveDataBufferSize;
			if (!buff_size)
				return false;
			memcpy(arr, gWaveDataBuffer + WAVE_BUFFER_SIZE - buff_size, llmin(U32(count), buff_size) * sizeof(float));
			gWaveDataMutex.unlock();
		}
		if (buff_size < U32(count))
			memset(arr + buff_size, 0, (count - buff_size) * sizeof(float));
	}
	return true;
}*/

///////////////////////////////////////////////////////
// manager of possibly-multiple internet audio streams

LLAudioStreamManagerFMODSTUDIO::LLAudioStreamManagerFMODSTUDIO(FMOD::System *system, FMOD::ChannelGroup *group, const std::string& url) :
mSystem(system),
mChannelGroup(group),
mStreamChannel(nullptr),
mInternetStream(nullptr),
mReady(false),
mInternetStreamURL(url)
{
	FMOD_RESULT result = mSystem->createStream(url.c_str(), FMOD_2D | FMOD_NONBLOCKING | FMOD_IGNORETAGS, nullptr, &mInternetStream);

	if (result != FMOD_OK)
	{
		LL_WARNS() << "Couldn't open fmod stream, error "
			<< FMOD_ErrorString(result)
			<< LL_ENDL;
		mReady = false;
		return;
	}

	mReady = true;
}

FMOD::Channel *LLAudioStreamManagerFMODSTUDIO::startStream()
{
	// We need a live and opened stream before we try and play it.
	FMOD_OPENSTATE open_state;
	if (getOpenState(open_state) != FMOD_OK || open_state != FMOD_OPENSTATE_READY)
	{
		LL_WARNS() << "No internet stream to start playing!" << LL_ENDL;
		return nullptr;
	}

	if (mStreamChannel)
		return mStreamChannel;	//Already have a channel for this stream.

	Check_FMOD_Stream_Error(mSystem->playSound(mInternetStream, mChannelGroup, true, &mStreamChannel), "FMOD::System::playSound");
	return mStreamChannel;
}

bool LLAudioStreamManagerFMODSTUDIO::stopStream()
{
	if (mInternetStream)
	{
		bool close = true;
		FMOD_OPENSTATE open_state;
		if (getOpenState(open_state) == FMOD_OK)
		{
			switch (open_state)
			{
			case FMOD_OPENSTATE_CONNECTING:
				close = false;
				break;
			default:
				close = true;
			}
		}

		if (close && mInternetStream->release() == FMOD_OK)
		{
			mStreamChannel = nullptr;
			mInternetStream = nullptr;
			return true;
		}
		else
		{
			return false;
		}
	}
	else
	{
		return true;
	}
}

FMOD_RESULT LLAudioStreamManagerFMODSTUDIO::getOpenState(FMOD_OPENSTATE& state, unsigned int* percentbuffered, bool* starving, bool* diskbusy)
{
	if (!mInternetStream)
		return FMOD_ERR_INVALID_HANDLE;
	FMOD_RESULT result = mInternetStream->getOpenState(&state, percentbuffered, starving, diskbusy);
	Check_FMOD_Stream_Error(result, "FMOD::Sound::getOpenState");
	return result;
}

void LLStreamingAudio_FMODSTUDIO::setBufferSizes(U32 streambuffertime, U32 decodebuffertime)
{
	Check_FMOD_Stream_Error(mSystem->setStreamBufferSize(streambuffertime / 1000 * 128 * 128, FMOD_TIMEUNIT_RAWBYTES), "FMOD::System::setStreamBufferSize");
	FMOD_ADVANCEDSETTINGS settings = {};
	settings.cbSize = sizeof(settings);
	settings.defaultDecodeBufferSize = decodebuffertime;//ms
	Check_FMOD_Stream_Error(mSystem->setAdvancedSettings(&settings), "FMOD::System::setAdvancedSettings");
}

bool LLStreamingAudio_FMODSTUDIO::releaseDeadStreams()
{
	// Kill dead internet streams, if possible
	std::list<LLAudioStreamManagerFMODSTUDIO *>::iterator iter;
	for (iter = mDeadStreams.begin(); iter != mDeadStreams.end();)
	{
		LLAudioStreamManagerFMODSTUDIO *streamp = *iter;
		if (streamp->stopStream())
		{
			LL_INFOS() << "Closed dead stream" << LL_ENDL;
			delete streamp;
			mDeadStreams.erase(iter++);
		}
		else
		{
			++iter;
		}
	}

	return mDeadStreams.empty();
}

void LLStreamingAudio_FMODSTUDIO::cleanupWaveData()
{
	if (mStreamGroup)
	{
		Check_FMOD_Stream_Error(mStreamGroup->release(), "FMOD::ChannelGroup::release");
		mStreamGroup = nullptr;
	}

	if (mStreamDSP)
		Check_FMOD_Stream_Error(mStreamDSP->release(), "FMOD::DSP::release");
	mStreamDSP = nullptr;
}