/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/audio/media_audio_loaders.h"

#include "media/audio/media_audio.h"
#include "media/audio/media_audio_ffmpeg_loader.h"
#include "media/audio/media_child_ffmpeg_loader.h"

namespace Media {
namespace Player {
namespace {

constexpr auto kPlaybackBufferSize = 256 * 1024;

} // namespace

Loaders::Loaders(QThread *thread)
: _fromExternalNotify([=] { videoSoundAdded(); }) {
	moveToThread(thread);
	_fromExternalNotify.moveToThread(thread);
	connect(thread, SIGNAL(started()), this, SLOT(onInit()));
	connect(thread, SIGNAL(finished()), this, SLOT(deleteLater()));
}

void Loaders::feedFromExternal(ExternalSoundPart &&part) {
	auto invoke = false;
	{
		QMutexLocker lock(&_fromExternalMutex);
		invoke = _fromExternalQueues.empty()
			&& _fromExternalForceToBuffer.empty();
		auto &queue = _fromExternalQueues[part.audio];
		queue.insert(
			end(queue),
			std::make_move_iterator(part.packets.begin()),
			std::make_move_iterator(part.packets.end()));
	}
	if (invoke) {
		_fromExternalNotify.call();
	}
}

void Loaders::forceToBufferExternal(const AudioMsgId &audioId) {
	auto invoke = false;
	{
		QMutexLocker lock(&_fromExternalMutex);
		invoke = _fromExternalQueues.empty()
			&& _fromExternalForceToBuffer.empty();
		_fromExternalForceToBuffer.emplace(audioId);
	}
	if (invoke) {
		_fromExternalNotify.call();
	}
}

void Loaders::videoSoundAdded() {
	auto queues = decltype(_fromExternalQueues)();
	auto forces = decltype(_fromExternalForceToBuffer)();
	{
		QMutexLocker lock(&_fromExternalMutex);
		queues = base::take(_fromExternalQueues);
		forces = base::take(_fromExternalForceToBuffer);
	}
	for (const auto &audioId : forces) {
		const auto tryLoader = [&](const auto &id, auto &loader) {
			if (audioId == id && loader) {
				loader->setForceToBuffer(true);
				if (loader->holdsSavedDecodedSamples()
					&& !queues.contains(audioId)) {
					loadData(audioId);
				}
				return true;
			}
			return false;
		};
		tryLoader(_audio, _audioLoader)
			|| tryLoader(_song, _songLoader)
			|| tryLoader(_video, _videoLoader);
	}
	for (auto &pair : queues) {
		const auto audioId = pair.first;
		auto &packets = pair.second;
		const auto tryLoader = [&](const auto &id, auto &loader) {
			if (id == audioId && loader) {
				loader->enqueuePackets(std::move(packets));
				if (loader->holdsSavedDecodedSamples()) {
					loadData(audioId);
				}
				return true;
			}
			return false;
		};
		tryLoader(_audio, _audioLoader)
			|| tryLoader(_song, _songLoader)
			|| tryLoader(_video, _videoLoader);
	}
}

void Loaders::onInit() {
}

void Loaders::onStart(const AudioMsgId &audio, qint64 positionMs) {
	auto type = audio.type();
	clear(type);
	{
		QMutexLocker lock(internal::audioPlayerMutex());
		if (!mixer()) return;

		auto track = mixer()->trackForType(type);
		if (!track) return;

		track->loading = true;
	}

	loadData(audio, positionMs);
}

AudioMsgId Loaders::clear(AudioMsgId::Type type) {
	AudioMsgId result;
	switch (type) {
	case AudioMsgId::Type::Voice:
		std::swap(result, _audio);
		_audioLoader = nullptr;
		break;
	case AudioMsgId::Type::Song:
		std::swap(result, _song);
		_songLoader = nullptr;
		break;
	case AudioMsgId::Type::Video:
		std::swap(result, _video);
		_videoLoader = nullptr;
		break;
	}
	return result;
}

void Loaders::setStoppedState(Mixer::Track *track, State state) {
	mixer()->setStoppedState(track, state);
}

void Loaders::emitError(AudioMsgId::Type type) {
	emit error(clear(type));
}

void Loaders::onLoad(const AudioMsgId &audio) {
	loadData(audio);
}

void Loaders::loadData(AudioMsgId audio, crl::time positionMs) {
	auto err = SetupNoErrorStarted;
	auto type = audio.type();
	auto l = setupLoader(audio, err, positionMs);
	if (!l) {
		if (err == SetupErrorAtStart) {
			emitError(type);
		}
		return;
	}

	auto started = (err == SetupNoErrorStarted);
	auto finished = false;
	auto waiting = false;
	auto errAtStart = started;

	QByteArray samples;
	int64 samplesCount = 0;
	if (l->holdsSavedDecodedSamples()) {
		l->takeSavedDecodedSamples(&samples, &samplesCount);
	}
	while (samples.size() < kPlaybackBufferSize) {
		auto res = l->readMore(samples, samplesCount);
		using Result = AudioPlayerLoader::ReadResult;
		if (res == Result::Error) {
			if (errAtStart) {
				{
					QMutexLocker lock(internal::audioPlayerMutex());
					if (auto track = checkLoader(type)) {
						track->state.state = State::StoppedAtStart;
					}
				}
				emitError(type);
				return;
			}
			finished = true;
			break;
		} else if (res == Result::EndOfFile) {
			finished = true;
			break;
		} else if (res == Result::Ok) {
			errAtStart = false;
		} else if (res == Result::Wait) {
			waiting = (samples.size() < kPlaybackBufferSize)
				&& (!samplesCount || !l->forceToBuffer());
			if (waiting) {
				l->saveDecodedSamples(&samples, &samplesCount);
			}
			break;
		}

		QMutexLocker lock(internal::audioPlayerMutex());
		if (!checkLoader(type)) {
			clear(type);
			return;
		}
	}

	QMutexLocker lock(internal::audioPlayerMutex());
	auto track = checkLoader(type);
	if (!track) {
		clear(type);
		return;
	}

	if (started || samplesCount) {
		Audio::AttachToDevice();
	}
	if (started) {
		track->started();
		if (!internal::audioCheckError()) {
			setStoppedState(track, State::StoppedAtStart);
			emitError(type);
			return;
		}

		track->format = l->format();
		track->frequency = l->samplesFrequency();

		const auto position = (positionMs * track->frequency) / 1000LL;
		track->bufferedPosition = position;
		track->state.position = position;
		track->fadeStartPosition = position;
	}
	if (samplesCount) {
		track->ensureStreamCreated(type);

		auto bufferIndex = track->getNotQueuedBufferIndex();

		if (!internal::audioCheckError()) {
			setStoppedState(track, State::StoppedAtError);
			emitError(type);
			return;
		}

		if (bufferIndex < 0) { // No free buffers, wait.
			l->saveDecodedSamples(&samples, &samplesCount);
			return;
		} else if (l->forceToBuffer()) {
			l->setForceToBuffer(false);
		}

		track->bufferSamples[bufferIndex] = samples;
		track->samplesCount[bufferIndex] = samplesCount;
		track->bufferedLength += samplesCount;
		alBufferData(track->stream.buffers[bufferIndex], track->format, samples.constData(), samples.size(), track->frequency);

		alSourceQueueBuffers(track->stream.source, 1, track->stream.buffers + bufferIndex);

		if (!internal::audioCheckError()) {
			setStoppedState(track, State::StoppedAtError);
			emitError(type);
			return;
		}
	} else {
		if (waiting) {
			return;
		}
		finished = true;
	}
	track->state.waitingForData = false;

	if (finished) {
		track->loaded = true;
		track->state.length = track->bufferedPosition + track->bufferedLength;
		clear(type);
	}

	track->loading = false;
	if (IsPausedOrPausing(track->state.state)
		|| IsStoppedOrStopping(track->state.state)) {
		return;
	}
	ALint state = AL_INITIAL;
	alGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);
	if (!internal::audioCheckError()) {
		setStoppedState(track, State::StoppedAtError);
		emitError(type);
		return;
	}

	if (state == AL_PLAYING) {
		return;
	} else if (state == AL_STOPPED && !internal::CheckAudioDeviceConnected()) {
		return;
	}

	alSourcef(track->stream.source, AL_GAIN, ComputeVolume(type));
	if (!internal::audioCheckError()) {
		setStoppedState(track, State::StoppedAtError);
		emitError(type);
		return;
	}

	if (state == AL_STOPPED) {
		alSourcei(track->stream.source, AL_SAMPLE_OFFSET, qMax(track->state.position - track->bufferedPosition, 0LL));
		if (!internal::audioCheckError()) {
			setStoppedState(track, State::StoppedAtError);
			emitError(type);
			return;
		}
	}
	alSourcePlay(track->stream.source);
	if (!internal::audioCheckError()) {
		setStoppedState(track, State::StoppedAtError);
		emitError(type);
		return;
	}

	emit needToCheck();
}

AudioPlayerLoader *Loaders::setupLoader(
		const AudioMsgId &audio,
		SetupError &err,
		crl::time positionMs) {
	err = SetupErrorAtStart;
	QMutexLocker lock(internal::audioPlayerMutex());
	if (!mixer()) return nullptr;

	auto track = mixer()->trackForType(audio.type());
	if (!track || track->state.id != audio || !track->loading) {
		emit error(audio);
		LOG(("Audio Error: trying to load part of audio, that is not current at the moment"));
		err = SetupErrorNotPlaying;
		return nullptr;
	}

	bool isGoodId = false;
	AudioPlayerLoader *l = nullptr;
	switch (audio.type()) {
	case AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (_audio == audio); break;
	case AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (_song == audio); break;
	case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (_video == audio); break;
	}

	if (l && (!isGoodId || !l->check(track->file, track->data))) {
		clear(audio.type());
		l = nullptr;
	}

	if (!l) {
		std::unique_ptr<AudioPlayerLoader> *loader = nullptr;
		switch (audio.type()) {
		case AudioMsgId::Type::Voice: _audio = audio; loader = &_audioLoader; break;
		case AudioMsgId::Type::Song: _song = audio; loader = &_songLoader; break;
		case AudioMsgId::Type::Video: _video = audio; loader = &_videoLoader; break;
		}

		if (audio.externalPlayId()) {
			if (!track->externalData) {
				clear(audio.type());
				track->state.state = State::StoppedAtError;
				emit error(audio);
				LOG(("Audio Error: video sound data not ready"));
				return nullptr;
			}
			*loader = std::make_unique<ChildFFMpegLoader>(
				std::move(track->externalData));
		} else {
			*loader = std::make_unique<FFMpegLoader>(
				track->file,
				track->data,
				bytes::vector());
		}
		l = loader->get();

		if (!l->open(positionMs)) {
			track->state.state = State::StoppedAtStart;
			return nullptr;
		}
		auto length = l->samplesCount();
		if (length <= 0) {
			track->state.state = State::StoppedAtStart;
			return nullptr;
		}
		track->state.length = length;
		track->state.frequency = l->samplesFrequency();
		err = SetupNoErrorStarted;
	} else if (track->loaded) {
		err = SetupErrorLoadedFull;
		LOG(("Audio Error: trying to load part of audio, that is already loaded to the end"));
		return nullptr;
	}
	return l;
}

Mixer::Track *Loaders::checkLoader(AudioMsgId::Type type) {
	if (!mixer()) return nullptr;

	auto track = mixer()->trackForType(type);
	auto isGoodId = false;
	AudioPlayerLoader *l = nullptr;
	switch (type) {
	case AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (track->state.id == _audio); break;
	case AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (track->state.id == _song); break;
	case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (track->state.id == _video); break;
	}
	if (!l || !track) return nullptr;

	if (!isGoodId || !track->loading || !l->check(track->file, track->data)) {
		LOG(("Audio Error: playing changed while loading"));
		return nullptr;
	}

	return track;
}

void Loaders::onCancel(const AudioMsgId &audio) {
	Expects(audio.type() != AudioMsgId::Type::Unknown);

	switch (audio.type()) {
	case AudioMsgId::Type::Voice: if (_audio == audio) clear(audio.type()); break;
	case AudioMsgId::Type::Song: if (_song == audio) clear(audio.type()); break;
	case AudioMsgId::Type::Video: if (_video == audio) clear(audio.type()); break;
	}

	QMutexLocker lock(internal::audioPlayerMutex());
	if (!mixer()) return;

	for (auto i = 0; i != kTogetherLimit; ++i) {
		auto track = mixer()->trackForType(audio.type(), i);
		if (track->state.id == audio) {
			track->loading = false;
		}
	}
}

Loaders::~Loaders() = default;

} // namespace Player
} // namespace Media