/**
 * Copyright (c) 2006-2015 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#include "Source.h"
#include "Pool.h"
#include "common/math.h"

// STD
#include <iostream>
#include <float.h>

namespace love
{
namespace audio
{
namespace openal
{

class InvalidFormatException : public love::Exception
{
public:

	InvalidFormatException(int channels, int bitdepth)
		: Exception("%d-channel Sources with %d bits per sample are not supported.", channels, bitdepth)
	{
	}

};

StaticDataBuffer::StaticDataBuffer(ALenum format, const ALvoid *data, ALsizei size, ALsizei freq)
{
	alGenBuffers(1, &buffer);
	alBufferData(buffer, format, data, size, freq);
}

StaticDataBuffer::~StaticDataBuffer()
{
	alDeleteBuffers(1, &buffer);
}

Source::Source(Pool *pool, love::sound::SoundData *soundData)
	: love::audio::Source(Source::TYPE_STATIC)
	, pool(pool)
	, valid(false)
	, staticBuffer(nullptr)
	, pitch(1.0f)
	, volume(1.0f)
	, relative(false)
	, looping(false)
	, paused(false)
	, minVolume(0.0f)
	, maxVolume(1.0f)
	, referenceDistance(1.0f)
	, rolloffFactor(1.0f)
	, maxDistance(FLT_MAX)
	, cone()
	, offsetSamples(0)
	, offsetSeconds(0)
	, sampleRate(soundData->getSampleRate())
	, channels(soundData->getChannels())
	, decoder(nullptr)
	, toLoop(0)
{
	ALenum fmt = getFormat(soundData->getChannels(), soundData->getBitDepth());
	if (fmt == 0)
		throw InvalidFormatException(soundData->getChannels(), soundData->getBitDepth());

	staticBuffer.set(new StaticDataBuffer(fmt, soundData->getData(), soundData->getSize(), soundData->getSampleRate()));

	// The buffer has a +2 retain count right now, but we want it to have +1.
	staticBuffer->release();

	float z[3] = {0, 0, 0};

	setFloatv(position, z);
	setFloatv(velocity, z);
	setFloatv(direction, z);
}

Source::Source(Pool *pool, love::sound::Decoder *decoder)
	: love::audio::Source(Source::TYPE_STREAM)
	, pool(pool)
	, valid(false)
	, staticBuffer(nullptr)
	, pitch(1.0f)
	, volume(1.0f)
	, relative(false)
	, looping(false)
	, paused(false)
	, minVolume(0.0f)
	, maxVolume(1.0f)
	, referenceDistance(1.0f)
	, rolloffFactor(1.0f)
	, maxDistance(FLT_MAX)
	, cone()
	, offsetSamples(0)
	, offsetSeconds(0)
	, sampleRate(decoder->getSampleRate())
	, channels(decoder->getChannels())
	, decoder(decoder)
	, toLoop(0)
{
	if (getFormat(decoder->getChannels(), decoder->getBitDepth()) == 0)
		throw InvalidFormatException(decoder->getChannels(), decoder->getBitDepth());

	alGenBuffers(MAX_BUFFERS, streamBuffers);

	float z[3] = {0, 0, 0};

	setFloatv(position, z);
	setFloatv(velocity, z);
	setFloatv(direction, z);
}

Source::Source(const Source &s)
	: love::audio::Source(s.type)
	, pool(s.pool)
	, valid(false)
	, staticBuffer(s.staticBuffer)
	, pitch(s.pitch)
	, volume(s.volume)
	, relative(s.relative)
	, looping(s.looping)
	, paused(false)
	, minVolume(s.minVolume)
	, maxVolume(s.maxVolume)
	, referenceDistance(s.referenceDistance)
	, rolloffFactor(s.rolloffFactor)
	, maxDistance(s.maxDistance)
	, cone(s.cone)
	, offsetSamples(0)
	, offsetSeconds(0)
	, sampleRate(s.sampleRate)
	, channels(s.channels)
	, decoder(nullptr)
	, toLoop(0)
{
	if (type == TYPE_STREAM)
	{
		if (s.decoder.get())
		{
			love::sound::Decoder *dec = s.decoder->clone();
			decoder.set(dec);
			dec->release();
		}

		alGenBuffers(MAX_BUFFERS, streamBuffers);
	}

	setFloatv(position, s.position);
	setFloatv(velocity, s.velocity);
	setFloatv(direction, s.direction);
}

Source::~Source()
{
	if (valid)
		pool->stop(this);

	if (type == TYPE_STREAM)
		alDeleteBuffers(MAX_BUFFERS, streamBuffers);
}

love::audio::Source *Source::clone()
{
	return new Source(*this);
}

bool Source::play()
{
	if (valid && paused)
	{
		pool->resume(this);
		return true;
	}

	valid = pool->play(this, source);
	return valid;
}

void Source::stop()
{
	if (!isStopped())
	{
		pool->stop(this);
		pool->softRewind(this);
	}
}

void Source::pause()
{
	pool->pause(this);
}

void Source::resume()
{
	pool->resume(this);
}

void Source::rewind()
{
	pool->rewind(this);
}

bool Source::isStopped() const
{
	if (valid)
	{
		ALenum state;
		alGetSourcei(source, AL_SOURCE_STATE, &state);
		return (state == AL_STOPPED);
	}

	return true;
}

bool Source::isPaused() const
{
	if (valid)
	{
		ALenum state;
		alGetSourcei(source, AL_SOURCE_STATE, &state);
		return (state == AL_PAUSED);
	}

	return false;
}

bool Source::isFinished() const
{
	return type == TYPE_STATIC ? isStopped() : isStopped() && !isLooping() && decoder->isFinished();
}

bool Source::update()
{
	if (!valid)
		return false;
	if (type == TYPE_STATIC)
	{
		// Looping mode could have changed.
		alSourcei(source, AL_LOOPING, isLooping() ? AL_TRUE : AL_FALSE);
		return !isStopped();
	}
	else if (type == TYPE_STREAM && (isLooping() || !isFinished()))
	{
		// Number of processed buffers.
		ALint processed = 0;

		alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);

		while (processed--)
		{
			ALuint buffer;

			float curOffsetSamples, curOffsetSecs;

			alGetSourcef(source, AL_SAMPLE_OFFSET, &curOffsetSamples);

			int freq = decoder->getSampleRate();
			curOffsetSecs = curOffsetSamples / freq;

			// Get a free buffer.
			alSourceUnqueueBuffers(source, 1, &buffer);

			float newOffsetSamples, newOffsetSecs;

			alGetSourcef(source, AL_SAMPLE_OFFSET, &newOffsetSamples);
			newOffsetSecs = newOffsetSamples / freq;

			offsetSamples += (curOffsetSamples - newOffsetSamples);
			offsetSeconds += (curOffsetSecs - newOffsetSecs);

			streamAtomic(buffer, decoder.get());
			alSourceQueueBuffers(source, 1, &buffer);
		}
		return true;
	}
	return false;
}

void Source::setPitch(float pitch)
{
	if (valid)
		alSourcef(source, AL_PITCH, pitch);

	this->pitch = pitch;
}

float Source::getPitch() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_PITCH, &f);
		return f;
	}

	// In case the Source isn't playing.
	return pitch;
}

void Source::setVolume(float volume)
{
	if (valid)
	{
		alSourcef(source, AL_GAIN, volume);
	}

	this->volume = volume;
}

float Source::getVolume() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_GAIN, &f);
		return f;
	}

	// In case the Source isn't playing.
	return volume;
}

void Source::seekAtomic(float offset, void *unit)
{
	if (valid)
	{
		switch (*((Source::Unit *) unit))
		{
		case Source::UNIT_SAMPLES:
			if (type == TYPE_STREAM)
			{
				offsetSamples = offset;
				offset /= decoder->getSampleRate();
				offsetSeconds = offset;
				decoder->seek(offset);
			}
			else
			{
				alSourcef(source, AL_SAMPLE_OFFSET, offset);
			}
			break;
		case Source::UNIT_SECONDS:
		default:
			if (type == TYPE_STREAM)
			{
				offsetSeconds = offset;
				decoder->seek(offset);
				offsetSamples = offset * decoder->getSampleRate();
			}
			else
			{
				alSourcef(source, AL_SEC_OFFSET, offset);
			}
			break;
		}
		if (type == TYPE_STREAM)
		{
			bool waspaused = paused;
			// Because we still have old data
			// from before the seek in the buffers
			// let's empty them.
			stopAtomic();
			playAtomic();
			if (waspaused)
				pauseAtomic();
		}
	}
}

void Source::seek(float offset, Source::Unit unit)
{
	return pool->seek(this, offset, &unit);
}

float Source::tellAtomic(void *unit) const
{
	if (valid)
	{
		float offset;
		switch (*((Source::Unit *) unit))
		{
		case Source::UNIT_SAMPLES:
			alGetSourcef(source, AL_SAMPLE_OFFSET, &offset);
			if (type == TYPE_STREAM) offset += offsetSamples;
			break;
		case Source::UNIT_SECONDS:
		default:
			{
				alGetSourcef(source, AL_SAMPLE_OFFSET, &offset);
				offset /= sampleRate;
				if (type == TYPE_STREAM) offset += offsetSeconds;
			}
			break;
		}
		return offset;
	}
	return 0.0f;
}

float Source::tell(Source::Unit unit)
{
	return pool->tell(this, &unit);
}

void Source::setPosition(float *v)
{
	if (valid)
		alSourcefv(source, AL_POSITION, v);

	setFloatv(position, v);
}

void Source::getPosition(float *v) const
{
	if (valid)
		alGetSourcefv(source, AL_POSITION, v);
	else
		setFloatv(v, position);
}

void Source::setVelocity(float *v)
{
	if (valid)
		alSourcefv(source, AL_VELOCITY, v);

	setFloatv(velocity, v);
}

void Source::getVelocity(float *v) const
{
	if (valid)
		alGetSourcefv(source, AL_VELOCITY, v);
	else
		setFloatv(v, velocity);
}

void Source::setDirection(float *v)
{
	if (valid)
		alSourcefv(source, AL_DIRECTION, v);
	else
		setFloatv(direction, v);
}

void Source::getDirection(float *v) const
{
	if (valid)
		alGetSourcefv(source, AL_DIRECTION, v);
	else
		setFloatv(v, direction);
}

void Source::setCone(float innerAngle, float outerAngle, float outerVolume)
{
	cone.innerAngle = (int) LOVE_TODEG(innerAngle);
	cone.outerAngle = (int) LOVE_TODEG(outerAngle);
	cone.outerVolume = outerVolume;

	if (valid)
	{
		alSourcei(source, AL_CONE_INNER_ANGLE, cone.innerAngle);
		alSourcei(source, AL_CONE_OUTER_ANGLE, cone.outerAngle);
		alSourcef(source, AL_CONE_OUTER_GAIN, cone.outerVolume);
	}
}

void Source::getCone(float &innerAngle, float &outerAngle, float &outerVolume) const
{
	innerAngle = LOVE_TORAD(cone.innerAngle);
	outerAngle = LOVE_TORAD(cone.outerAngle);
	outerVolume = cone.outerVolume;
}

void Source::setRelative(bool enable)
{
	if (valid)
		alSourcei(source, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE);

	relative = enable;
}

bool Source::isRelative() const
{
	return relative;
}

void Source::setLooping(bool looping)
{
	if (valid && type == TYPE_STATIC)
		alSourcei(source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);

	this->looping = looping;
}

bool Source::isLooping() const
{
	return looping;
}

bool Source::playAtomic()
{
	if (type == TYPE_STATIC)
	{
		alSourcei(source, AL_BUFFER, staticBuffer->getBuffer());
	}
	else if (type == TYPE_STREAM)
	{
		int usedBuffers = 0;

		for (unsigned int i = 0; i < MAX_BUFFERS; i++)
		{
			streamAtomic(streamBuffers[i], decoder.get());
			++usedBuffers;
			if (decoder->isFinished())
				break;
		}

		if (usedBuffers > 0)
			alSourceQueueBuffers(source, usedBuffers, streamBuffers);
	}

	// This Source may now be associated with an OpenAL source that still has
	// the properties of another love Source. Let's reset it to the settings
	// of the new one.
	reset();

	// Clear errors.
	alGetError();

	alSourcePlay(source);

	// alSourcePlay may fail if the system has reached its limit of simultaneous
	// playing sources.
	bool success = alGetError() == AL_NO_ERROR;

	valid = true; //if it fails it will be set to false again
	//but this prevents a horrible, horrible bug

	return success;
}

void Source::stopAtomic()
{
	if (valid)
	{
		if (type == TYPE_STATIC)
		{
			alSourceStop(source);
		}
		else if (type == TYPE_STREAM)
		{
			alSourceStop(source);
			int queued = 0;
			alGetSourcei(source, AL_BUFFERS_QUEUED, &queued);

			while (queued--)
			{
				ALuint buffer;
				alSourceUnqueueBuffers(source, 1, &buffer);
			}
		}
		alSourcei(source, AL_BUFFER, AL_NONE);
	}
	toLoop = 0;
	valid = false;
}

void Source::pauseAtomic()
{
	if (valid)
	{
		alSourcePause(source);
		paused = true;
	}
}

void Source::resumeAtomic()
{
	if (valid && paused)
	{
		alSourcePlay(source);
		paused = false;
	}
}

void Source::rewindAtomic()
{
	if (valid && type == TYPE_STATIC)
	{
		alSourceRewind(source);
		if (!paused)
			alSourcePlay(source);
	}
	else if (valid && type == TYPE_STREAM)
	{
		bool waspaused = paused;
		decoder->rewind();
		// Because we still have old data
		// from before the seek in the buffers
		// let's empty them.
		stopAtomic();
		playAtomic();
		if (waspaused)
			pauseAtomic();
		offsetSamples = 0;
		offsetSeconds = 0;
	}
	else if (type == TYPE_STREAM)
	{
		decoder->rewind();
		offsetSamples = 0;
		offsetSeconds = 0;
	}
}

void Source::reset()
{
	alSourcefv(source, AL_POSITION, position);
	alSourcefv(source, AL_VELOCITY, velocity);
	alSourcefv(source, AL_DIRECTION, direction);
	alSourcef(source, AL_PITCH, pitch);
	alSourcef(source, AL_GAIN, volume);
	alSourcef(source, AL_MIN_GAIN, minVolume);
	alSourcef(source, AL_MAX_GAIN, maxVolume);
	alSourcef(source, AL_REFERENCE_DISTANCE, referenceDistance);
	alSourcef(source, AL_ROLLOFF_FACTOR, rolloffFactor);
	alSourcef(source, AL_MAX_DISTANCE, maxDistance);
	alSourcei(source, AL_LOOPING, isStatic() && isLooping() ? AL_TRUE : AL_FALSE);
	alSourcei(source, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE);
	alSourcei(source, AL_CONE_INNER_ANGLE, cone.innerAngle);
	alSourcei(source, AL_CONE_OUTER_ANGLE, cone.outerAngle);
	alSourcef(source, AL_CONE_OUTER_GAIN, cone.outerVolume);
}

void Source::setFloatv(float *dst, const float *src) const
{
	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
}

ALenum Source::getFormat(int channels, int bitDepth) const
{
	if (channels == 1 && bitDepth == 8)
		return AL_FORMAT_MONO8;
	else if (channels == 1 && bitDepth == 16)
		return AL_FORMAT_MONO16;
	else if (channels == 2 && bitDepth == 8)
		return AL_FORMAT_STEREO8;
	else if (channels == 2 && bitDepth == 16)
		return AL_FORMAT_STEREO16;

#ifdef AL_EXT_MCFORMATS
	if (alIsExtensionPresent("AL_EXT_MCFORMATS"))
	{
		if (channels == 6 && bitDepth == 8)
			return AL_FORMAT_51CHN8;
		else if (channels == 6 && bitDepth == 16)
			return AL_FORMAT_51CHN16;
		else if (channels == 8 && bitDepth == 8)
			return AL_FORMAT_71CHN8;
		else if (channels == 8 && bitDepth == 16)
			return AL_FORMAT_71CHN16;
	}
#endif

	return 0;
}

int Source::streamAtomic(ALuint buffer, love::sound::Decoder *d)
{
	// Get more sound data.
	int decoded = d->decode();
	decoded = decoded >= 0 ? decoded : 0;

	int fmt = getFormat(d->getChannels(), d->getBitDepth());

	if (fmt != 0)
		alBufferData(buffer, fmt, d->getBuffer(), decoded, d->getSampleRate());

	if (decoder->isFinished() && isLooping())
	{
		int queued, processed;
		alGetSourcei(source, AL_BUFFERS_QUEUED, &queued);
		alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
		if (queued > processed)
			toLoop = queued-processed;
		else
			toLoop = MAX_BUFFERS-processed;
		d->rewind();
	}

	if (toLoop > 0)
	{
		if (--toLoop == 0)
		{
			offsetSamples = 0;
			offsetSeconds = 0;
		}
	}

	return decoded;
}

bool Source::isStatic() const
{
	return (type == TYPE_STATIC);
}

void Source::setMinVolume(float volume)
{
	if (valid)
	{
		alSourcef(source, AL_MIN_GAIN, volume);
	}

	this->minVolume = volume;
}

float Source::getMinVolume() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_MIN_GAIN, &f);
		return f;
	}

	// In case the Source isn't playing.
	return this->minVolume;
}

void Source::setMaxVolume(float volume)
{
	if (valid)
	{
		alSourcef(source, AL_MAX_GAIN, volume);
	}

	this->maxVolume = volume;
}

float Source::getMaxVolume() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_MAX_GAIN, &f);
		return f;
	}

	// In case the Source isn't playing.
	return this->maxVolume;
}

void Source::setReferenceDistance(float distance)
{
	if (valid)
	{
		alSourcef(source, AL_REFERENCE_DISTANCE, distance);
	}

	this->referenceDistance = distance;
}

float Source::getReferenceDistance() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_REFERENCE_DISTANCE, &f);
		return f;
	}

	// In case the Source isn't playing.
	return this->referenceDistance;
}

void Source::setRolloffFactor(float factor)
{
	if (valid)
	{
		alSourcef(source, AL_ROLLOFF_FACTOR, factor);
	}

	this->rolloffFactor = factor;
}

float Source::getRolloffFactor() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_ROLLOFF_FACTOR, &f);
		return f;
	}

	// In case the Source isn't playing.
	return this->rolloffFactor;
}

void Source::setMaxDistance(float distance)
{
	if (valid)
	{
		alSourcef(source, AL_MAX_DISTANCE, distance);
	}

	this->maxDistance = distance;
}

float Source::getMaxDistance() const
{
	if (valid)
	{
		ALfloat f;
		alGetSourcef(source, AL_MAX_DISTANCE, &f);
		return f;
	}

	// In case the Source isn't playing.
	return this->maxDistance;
}

int Source::getChannels() const
{
	return channels;
}

} // openal
} // audio
} // love
