// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "webrtc/details/webrtc_openal_adm.h"

#include "base/timer.h"
#include "base/invoke_queued.h"
#include "webrtc/webrtc_device_common.h"

#include <crl/crl_semaphore.h>

#undef emit
#undef slots
#undef signals

#include <rtc_base/logging.h>
#include <rtc_base/thread.h>
#include <QtCore/QPointer>
#include <QtCore/QThread>

#ifdef WEBRTC_WIN
#include "webrtc/platform/win/webrtc_loopback_adm_win.h"
#endif // WEBRTC_WIN

namespace Webrtc::details {
namespace {

constexpr auto kRecordingFrequency = 48000;
constexpr auto kPlayoutFrequency = 48000;
constexpr auto kRecordingChannels = 1;
constexpr auto kBufferSizeMs = crl::time(10);
constexpr auto kPlayoutPart = (kPlayoutFrequency * kBufferSizeMs + 999)
	/ 1000;
constexpr auto kRecordingPart = (kRecordingFrequency * kBufferSizeMs + 999)
	/ 1000;
constexpr auto kRecordingBufferSize = kRecordingPart * sizeof(int16_t)
	* kRecordingChannels;
constexpr auto kRestartAfterEmptyData = 50; // Half a second with no data.
constexpr auto kProcessInterval = crl::time(10);

constexpr auto kBuffersFullCount = 7;
constexpr auto kBuffersKeepReadyCount = 5;

constexpr auto kDefaultRecordingLatency = crl::time(20);
constexpr auto kDefaultPlayoutLatency = crl::time(20);
constexpr auto kQueryExactTimeEach = 20;

constexpr auto kALMaxValues = 6;
auto kAL_EVENT_CALLBACK_FUNCTION_SOFT = ALenum();
auto kAL_EVENT_CALLBACK_USER_PARAM_SOFT = ALenum();
auto kAL_EVENT_TYPE_BUFFER_COMPLETED_SOFT = ALenum();
auto kAL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT = ALenum();
auto kAL_EVENT_TYPE_DISCONNECTED_SOFT = ALenum();
auto kAL_SAMPLE_OFFSET_CLOCK_SOFT = ALenum();
auto kAL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT = ALenum();

auto kALC_DEVICE_LATENCY_SOFT = ALenum();

using AL_INT64_TYPE = std::int64_t;

using ALEVENTPROCSOFT = void(*)(
	ALenum eventType,
	ALuint object,
	ALuint param,
	ALsizei length,
	const ALchar *message,
	void *userParam);
using ALEVENTCALLBACKSOFT = void(*)(
	ALEVENTPROCSOFT callback,
	void *userParam);
using ALCSETTHREADCONTEXT = ALCboolean(*)(ALCcontext *context);
using ALGETSOURCEI64VSOFT = void(*)(
	ALuint source,
	ALenum param,
	AL_INT64_TYPE *values);
using ALCGETINTEGER64VSOFT = void(*)(
	ALCdevice *device,
	ALCenum pname,
	ALsizei size,
	AL_INT64_TYPE *values);

ALEVENTCALLBACKSOFT alEventCallbackSOFT/* = nullptr*/;
ALCSETTHREADCONTEXT alcSetThreadContext/* = nullptr*/;
ALGETSOURCEI64VSOFT alGetSourcei64vSOFT/* = nullptr*/;
ALCGETINTEGER64VSOFT alcGetInteger64vSOFT/* = nullptr*/;

[[nodiscard]] bool Failed(ALCdevice *device) {
	if (auto code = alcGetError(device); code != ALC_NO_ERROR) {
		RTC_LOG(LS_ERROR)
			<< "OpenAL Error "
			<< code
			<< ": "
			<< (const char *)alcGetString(device, code);
		return true;
	}
	return false;
}

template <typename Callback>
void EnumerateDevices(ALCenum specifier, Callback &&callback) {
	auto devices = alcGetString(nullptr, specifier);
	Assert(devices != nullptr);
	while (*devices != 0) {
		callback(devices);
		while (*devices != 0) {
			++devices;
		}
		++devices;
	}
}

[[nodiscard]] int DevicesCount(ALCenum specifier) {
	auto result = 0;
	EnumerateDevices(specifier, [&](const char *device) {
		++result;
	});
	return result;
}

[[nodiscard]] int DeviceName(
		ALCenum specifier,
		int index,
		std::string *name,
		std::string *guid) {
	EnumerateDevices(specifier, [&](const char *device) {
		if (index < 0) {
			return;
		} else if (index > 0) {
			--index;
			return;
		}

		auto string = std::string(device);
		if (name) {
			if (guid) {
				*guid = string;
			}
			const auto prefix = std::string("OpenAL Soft on ");
			if (string.rfind(prefix, 0) == 0) {
				string = string.substr(prefix.size());
			}
			*name = std::move(string);
		} else if (guid) {
			*guid = std::move(string);
		}
		index = -1;
	});
	return (index > 0) ? -1 : 0;
}

void SetStringToArray(const std::string &string, char *array, int size) {
	const auto length = std::min(int(string.size()), size - 1);
	if (length > 0) {
		memcpy(array, string.data(), length);
	}
	array[length] = 0;
}

[[nodiscard]] int DeviceName(
		ALCenum specifier,
		int index,
		char name[webrtc::kAdmMaxDeviceNameSize],
		char guid[webrtc::kAdmMaxGuidSize]) {
	auto sname = std::string();
	auto sguid = std::string();
	const auto result = DeviceName(specifier, index, &sname, &sguid);
	if (result) {
		return result;
	}
	SetStringToArray(sname, name, webrtc::kAdmMaxDeviceNameSize);
	SetStringToArray(sguid, guid, webrtc::kAdmMaxGuidSize);
	return 0;
}

} // namespace

struct AudioDeviceOpenAL::Data {
	Data() : timer(&thread) {
		context.moveToThread(&thread);
	}

	QThread thread;
	QObject context;
	base::Timer timer;

	QByteArray recordedSamples;
	int emptyRecordingData = 0;
	bool recording = false;

	QByteArray playoutSamples;
	ALuint source = 0;
	int queuedBuffersCount = 0;
	std::array<ALuint, kBuffersFullCount> buffers = { { 0 } };
	std::array<bool, kBuffersFullCount> queuedBuffers = { { false } };
	int64_t exactDeviceTimeCounter = 0;
	int64_t lastExactDeviceTime = 0;
	crl::time lastExactDeviceTimeWhen = 0;
	bool playing = false;
};

template <typename Callback>
std::invoke_result_t<Callback> AudioDeviceOpenAL::sync(Callback &&callback) {
	Expects(_data != nullptr);

	using Result = std::invoke_result_t<Callback>;

	crl::semaphore semaphore;
	if constexpr (std::is_same_v<Result, void>) {
		InvokeQueued(&_data->context, [&] {
			callback();
			semaphore.release();
		});
		semaphore.acquire();
	} else {
		auto result = Result();
		InvokeQueued(&_data->context, [&] {
			result = callback();
			semaphore.release();
		});
		semaphore.acquire();
		return result;
	}
}

AudioDeviceOpenAL::AudioDeviceOpenAL(
	webrtc::TaskQueueFactory *taskQueueFactory)
: _audioDeviceBuffer(taskQueueFactory)
, _deviceResolvedIds(std::make_shared<DeviceResolvedIds>()) {
	_audioDeviceBuffer.SetRecordingSampleRate(kRecordingFrequency);
	_audioDeviceBuffer.SetRecordingChannels(kRecordingChannels);
}

AudioDeviceOpenAL::~AudioDeviceOpenAL() {
	Terminate();
}

Fn<void(DeviceResolvedId)> AudioDeviceOpenAL::setDeviceIdCallback() {
	return [weak = std::weak_ptr<DeviceResolvedIds>(_deviceResolvedIds)](
			DeviceResolvedId id) {
		if (const auto shared = weak.lock()) {
			auto lock = QMutexLocker(&shared->mutex);
			(id.type == DeviceType::Playback
				? shared->playback
				: shared->capture) = std::move(id);
		}
	};
}

int32_t AudioDeviceOpenAL::ActiveAudioLayer(AudioLayer *audioLayer) const {
	*audioLayer = kPlatformDefaultAudio;
	return 0;
}

int32_t AudioDeviceOpenAL::RegisterAudioCallback(
		webrtc::AudioTransport *audioCallback) {
	return _audioDeviceBuffer.RegisterAudioCallback(audioCallback);
}

int32_t AudioDeviceOpenAL::Init() {
	if (_initialized) {
		return 0;
	}
	alcSetThreadContext = (ALCSETTHREADCONTEXT)alcGetProcAddress(
		nullptr,
		"alcSetThreadContext");
	if (!alcSetThreadContext) {
		return -1;
	}
	alEventCallbackSOFT = (ALEVENTCALLBACKSOFT)alGetProcAddress(
		"alEventCallbackSOFT");

	alGetSourcei64vSOFT = (ALGETSOURCEI64VSOFT)alGetProcAddress(
		"alGetSourcei64vSOFT");

	alcGetInteger64vSOFT = (ALCGETINTEGER64VSOFT)alcGetProcAddress(
		nullptr,
		"alcGetInteger64vSOFT");

#define RESOLVE_AL_ENUM(ENUM) k##ENUM = alGetEnumValue(#ENUM)
#define RESOLVE_ALC_ENUM(ENUM) k##ENUM = alcGetEnumValue(nullptr, #ENUM)
	RESOLVE_AL_ENUM(AL_EVENT_CALLBACK_FUNCTION_SOFT);
	RESOLVE_AL_ENUM(AL_EVENT_CALLBACK_FUNCTION_SOFT);
	RESOLVE_AL_ENUM(AL_EVENT_CALLBACK_USER_PARAM_SOFT);
	RESOLVE_AL_ENUM(AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT);
	RESOLVE_AL_ENUM(AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT);
	RESOLVE_AL_ENUM(AL_EVENT_TYPE_DISCONNECTED_SOFT);
	RESOLVE_AL_ENUM(AL_SAMPLE_OFFSET_CLOCK_SOFT);
	RESOLVE_AL_ENUM(AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT);
	RESOLVE_ALC_ENUM(ALC_DEVICE_LATENCY_SOFT);
#undef RESOLVE_ALC_ENUM
#undef RESOLVE_AL_ENUM

	_initialized = true;
	return 0;
}

int32_t AudioDeviceOpenAL::Terminate() {
	StopRecording();
	StopPlayout();
	_initialized = false;

	Ensures(!_data);
	return 0;
}

bool AudioDeviceOpenAL::Initialized() const {
	return _initialized;
}

int32_t AudioDeviceOpenAL::InitSpeaker() {
	_speakerInitialized = true;
	return 0;
}

int32_t AudioDeviceOpenAL::InitMicrophone() {
	_microphoneInitialized = true;
	return 0;
}

bool AudioDeviceOpenAL::SpeakerIsInitialized() const {
	return _speakerInitialized;
}

bool AudioDeviceOpenAL::MicrophoneIsInitialized() const {
	return _microphoneInitialized;
}

int32_t AudioDeviceOpenAL::SpeakerVolumeIsAvailable(bool *available) {
	if (available) {
		*available = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetSpeakerVolume(uint32_t volume) {
	return -1;
}

int32_t AudioDeviceOpenAL::SpeakerVolume(uint32_t *volume) const {
	return -1;
}

int32_t AudioDeviceOpenAL::MaxSpeakerVolume(uint32_t *maxVolume) const {
	return -1;
}

int32_t AudioDeviceOpenAL::MinSpeakerVolume(uint32_t *minVolume) const {
	return -1;
}

int32_t AudioDeviceOpenAL::SpeakerMuteIsAvailable(bool *available) {
	if (available) {
		*available = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetSpeakerMute(bool enable) {
	return -1;
}

int32_t AudioDeviceOpenAL::SpeakerMute(bool *enabled) const {
	if (enabled) {
		*enabled = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::MicrophoneMuteIsAvailable(bool *available) {
	if (available) {
		*available = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetMicrophoneMute(bool enable) {
	return -1;
}

int32_t AudioDeviceOpenAL::MicrophoneMute(bool *enabled) const {
	if (enabled) {
		*enabled = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::StereoRecordingIsAvailable(
		bool *available) const {
	if (available) {
		*available = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetStereoRecording(bool enable) {
	return -1;
}

int32_t AudioDeviceOpenAL::StereoRecording(bool *enabled) const {
	if (enabled) {
		*enabled = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::StereoPlayoutIsAvailable(bool *available) const {
	if (available) {
		*available = true;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetStereoPlayout(bool enable) {
	if (Playing()) {
		return -1;
	}
	_playoutChannels = enable ? 2 : 1;
	return 0;
}

int32_t AudioDeviceOpenAL::StereoPlayout(bool *enabled) const {
	if (enabled) {
		*enabled = (_playoutChannels == 2);
	}
	return 0;
}

int32_t AudioDeviceOpenAL::MicrophoneVolumeIsAvailable(
		bool *available) {
	if (available) {
		*available = false;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::SetMicrophoneVolume(uint32_t volume) {
	return -1;
}

int32_t AudioDeviceOpenAL::MicrophoneVolume(uint32_t *volume) const {
	return -1;
}

int32_t AudioDeviceOpenAL::MaxMicrophoneVolume(uint32_t *maxVolume) const {
	return -1;
}

int32_t AudioDeviceOpenAL::MinMicrophoneVolume(uint32_t *minVolume) const {
	return -1;
}

int16_t AudioDeviceOpenAL::PlayoutDevices() {
	return DevicesCount(ALC_ALL_DEVICES_SPECIFIER);
}

int32_t AudioDeviceOpenAL::SetPlayoutDevice(uint16_t index) {
	// We should've receive the id through setDeviceIdCallback by now.
	return restartPlayout();
}

int32_t AudioDeviceOpenAL::SetPlayoutDevice(WindowsDeviceType /*device*/) {
	// We should've receive the id through setDeviceIdCallback by now.
	return restartPlayout();
}

int32_t AudioDeviceOpenAL::PlayoutDeviceName(
		uint16_t index,
		char name[webrtc::kAdmMaxDeviceNameSize],
		char guid[webrtc::kAdmMaxGuidSize]) {
	return DeviceName(ALC_ALL_DEVICES_SPECIFIER, index, name, guid);
}

int32_t AudioDeviceOpenAL::RecordingDeviceName(
		uint16_t index,
		char name[webrtc::kAdmMaxDeviceNameSize],
		char guid[webrtc::kAdmMaxGuidSize]) {
	return DeviceName(ALC_CAPTURE_DEVICE_SPECIFIER, index, name, guid);
}

int16_t AudioDeviceOpenAL::RecordingDevices() {
	return DevicesCount(ALC_CAPTURE_DEVICE_SPECIFIER);
}

int32_t AudioDeviceOpenAL::SetRecordingDevice(uint16_t index) {
	return restartRecording();
}

int32_t AudioDeviceOpenAL::SetRecordingDevice(WindowsDeviceType /*device*/) {
	// We should've receive the id through setDeviceIdCallback by now.
	return restartRecording();
}

int32_t AudioDeviceOpenAL::PlayoutIsAvailable(bool *available) {
	if (available) {
		*available = true;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::RecordingIsAvailable(bool *available) {
	if (available) {
		*available = true;
	}
	return 0;
}

int32_t AudioDeviceOpenAL::InitPlayout() {
	if (!_initialized) {
		return -1;
	} else if (_playoutInitialized) {
		return 0;
	}
	_playoutInitialized = true;
	ensureThreadStarted();
	openPlayoutDevice();
	return 0;
}

void AudioDeviceOpenAL::openRecordingDevice() {
	if (_recordingDevice || _recordingFailed) {
		return;
	}
	auto lock = QMutexLocker(&_deviceResolvedIds->mutex);
	const auto id = _deviceResolvedIds->capture;
	lock.unlock();

	const auto utf = id.isDefault() ? std::string() : id.value.toStdString();
	_recordingDevice = alcCaptureOpenDevice(
		utf.empty() ? nullptr : utf.c_str(),
		kRecordingFrequency,
		AL_FORMAT_MONO16,
		kRecordingFrequency / 4);
	if (!_recordingDevice) {
		RTC_LOG(LS_ERROR)
			<< "OpenAL Capture Device open failed, deviceID: '"
			<< utf
			<< "'";
		_recordingFailed = true;
		return;
	}
	// This does not work for capture devices :(
	//_context = alcCreateContext(_device, nullptr);
	//	alEventCallbackSOFT([](
	//			ALenum eventType,
	//			ALuint object,
	//			ALuint param,
	//			ALsizei length,
	//			const ALchar *message,
	//			void *that) {
	//		static_cast<AudioInputOpenAL*>(that)->handleEvent(
	//			eventType,
	//			object,
	//			param,
	//			length,
	//			message);
	//	}, this);
}

void AudioDeviceOpenAL::openPlayoutDevice() {
	if (_playoutDevice || _playoutFailed) {
		return;
	}
	auto lock = QMutexLocker(&_deviceResolvedIds->mutex);
	const auto id = _deviceResolvedIds->playback;
	lock.unlock();

	const auto utf = id.isDefault() ? std::string() : id.value.toStdString();
	_playoutDevice = alcOpenDevice(utf.empty() ? nullptr : utf.c_str());
	if (!_playoutDevice) {
		RTC_LOG(LS_ERROR)
			<< "OpenAL Device open failed, deviceID: '"
			<< utf
			<< "'";
		_playoutFailed = true;
		return;
	}
	_playoutContext = alcCreateContext(_playoutDevice, nullptr);
	if (!_playoutContext) {
		RTC_LOG(LS_ERROR) << "OpenAL Context create failed.";
		_playoutFailed = true;
		closePlayoutDevice();
		return;
	}
	sync([&] {
		alcSetThreadContext(_playoutContext);
		if (alEventCallbackSOFT) {
			alEventCallbackSOFT([](
					ALenum eventType,
					ALuint object,
					ALuint param,
					ALsizei length,
					const ALchar *message,
					void *that) {
				static_cast<AudioDeviceOpenAL*>(that)->handleEvent(
					eventType,
					object,
					param,
					length,
					message);
			}, this);
		}
	});
}

void AudioDeviceOpenAL::handleEvent(
		ALenum eventType,
		ALuint object,
		ALuint param,
		ALsizei length,
		const ALchar *message) {
	if (eventType == kAL_EVENT_TYPE_DISCONNECTED_SOFT && _thread) {
		const auto weak = QPointer<QObject>(&_data->context);
		_thread->PostTask([=] {
			if (weak) {
				restartRecording();
			}
		});
	}
}

int32_t AudioDeviceOpenAL::InitRecording() {
	if (!_initialized) {
		return -1;
	} else if (_recordingInitialized) {
		return 0;
	}
	_recordingInitialized = true;
	ensureThreadStarted();
	openRecordingDevice();
	_audioDeviceBuffer.SetRecordingSampleRate(kRecordingFrequency);
	_audioDeviceBuffer.SetRecordingChannels(kRecordingChannels);
	return 0;
}

void AudioDeviceOpenAL::ensureThreadStarted() {
	if (_data) {
		return;
	}
	_thread = rtc::Thread::Current();
	if (_thread && !_thread->IsOwned()) {
		_thread->UnwrapCurrent();
		_thread = nullptr;
	}
	//	Assert(_thread != nullptr);
	//	Assert(_thread->IsOwned());

	_data = std::make_unique<Data>();
	_data->timer.setCallback([=] { processData(); });
	_data->thread.setObjectName("Webrtc OpenAL Thread");
	_data->thread.start(QThread::TimeCriticalPriority);
}

void AudioDeviceOpenAL::processData() {
	Expects(_data != nullptr);

	if (_data->playing && !_playoutFailed) {
		processPlayoutData();
	}
	if (_data->recording && !_recordingFailed) {
		processRecordingData();
	}
}

bool AudioDeviceOpenAL::processRecordedPart(bool firstInCycle) {
	auto samples = ALint();
	alcGetIntegerv(_recordingDevice, ALC_CAPTURE_SAMPLES, 1, &samples);
	if (Failed(_recordingDevice)) {
		restartRecordingQueued();
		return false;
	}
	if (samples <= 0) {
		if (firstInCycle) {
			++_data->emptyRecordingData;
			if (_data->emptyRecordingData == kRestartAfterEmptyData) {
				restartRecordingQueued();
			}
		}
		return false;
	} else if (samples < kRecordingPart) {
		// Not enough data for 10ms.
		return false;
	}

	_recordingLatency = queryRecordingLatencyMs();
	//RTC_LOG(LS_ERROR) << "RECORDING LATENCY: " << _recordingLatency << "ms";

	_data->emptyRecordingData = 0;
	if (_data->recordedSamples.size() < kRecordingBufferSize) {
		_data->recordedSamples.resize(kRecordingBufferSize);
	}
	alcCaptureSamples(
		_recordingDevice,
		_data->recordedSamples.data(),
		kRecordingPart);
	if (Failed(_recordingDevice)) {
		restartRecordingQueued();
		return false;
	}
	_audioDeviceBuffer.SetRecordedBuffer(
		_data->recordedSamples.data(),
		kRecordingPart);
	_audioDeviceBuffer.SetVQEData(_playoutLatency, _recordingLatency);
	_audioDeviceBuffer.DeliverRecordedData();
	return true;
}

void AudioDeviceOpenAL::processRecordingData() {
	for (auto first = true; processRecordedPart(first); first = false) {
	}
}

bool AudioDeviceOpenAL::clearProcessedBuffer() {
	Expects(_data != nullptr);

	auto processed = ALint(0);
	alGetSourcei(_data->source, AL_BUFFERS_PROCESSED, &processed);
	if (processed < 1) {
		return false;
	}
	auto buffer = ALuint(0);
	alSourceUnqueueBuffers(_data->source, 1, &buffer);
	for (auto i = 0; i != int(_data->buffers.size()); ++i) {
		if (_data->buffers[i] == buffer) {
			_data->queuedBuffers[i] = false;
			--_data->queuedBuffersCount;
			return true;
		}
	}
	Unexpected("Processed buffer not found.");
}

void AudioDeviceOpenAL::unqueueAllBuffers() {
	alSourcei(_data->source, AL_BUFFER, AL_NONE);
	ranges::fill(_data->queuedBuffers, false);
	_data->queuedBuffersCount = 0;
}

void AudioDeviceOpenAL::clearProcessedBuffers() {
	while (true) {
		if (!clearProcessedBuffer()) {
			break;
		}
	}
}

crl::time AudioDeviceOpenAL::queryRecordingLatencyMs() {
#ifdef WEBRTC_WIN
	if (kALC_DEVICE_LATENCY_SOFT
		&& kAL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT) { // Check patched build.
		auto latency = AL_INT64_TYPE();
		alcGetInteger64vSOFT(
			_recordingDevice,
			kALC_DEVICE_LATENCY_SOFT,
			1,
			&latency);
		return latency / 1'000'000;
	}
#endif // WEBRTC_WIN
	return kDefaultRecordingLatency;
}

crl::time AudioDeviceOpenAL::countExactQueuedMsForLatency(
		crl::time now,
		bool playing) {
	auto values = std::array<AL_INT64_TYPE, kALMaxValues>{};
	auto &sampleOffset = values[0];
	auto &clockTime = values[1];
	auto &exactDeviceTime = values[2];
	const auto countExact = alGetSourcei64vSOFT
		&& kAL_SAMPLE_OFFSET_CLOCK_SOFT
		&& kAL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT;
	if (countExact) {
		if (!_data->lastExactDeviceTimeWhen
			|| !(++_data->exactDeviceTimeCounter % kQueryExactTimeEach)) {
			alGetSourcei64vSOFT(
				_data->source,
				kAL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT,
				values.data());
			_data->lastExactDeviceTime = exactDeviceTime;
			_data->lastExactDeviceTimeWhen = now;
		} else {
			alGetSourcei64vSOFT(
				_data->source,
				kAL_SAMPLE_OFFSET_CLOCK_SOFT,
				values.data());

			// The exactDeviceTime is in nanoseconds.
			exactDeviceTime = _data->lastExactDeviceTime
				+ (now - _data->lastExactDeviceTimeWhen) * 1'000'000;
		}
	} else {
		auto offset = ALint(0);
		alGetSourcei(_data->source, AL_SAMPLE_OFFSET, &offset);
		sampleOffset = (AL_INT64_TYPE(offset) << 32);
	}

	const auto queuedSamples = (AL_INT64_TYPE(
		_data->queuedBuffersCount * kPlayoutPart) << 32);
	const auto processedInOpenAL = playing ? sampleOffset : queuedSamples;
	const auto secondsQueuedInDevice = std::max(
		clockTime - exactDeviceTime,
		AL_INT64_TYPE(0)
	) / 1'000'000'000.;
	const auto secondsQueuedInOpenAL
		= (double((queuedSamples - processedInOpenAL) >> (32 - 10))
			/ double(kPlayoutFrequency * (1 << 10)));

	const auto queuedTotal = crl::time(base::SafeRound(
		(secondsQueuedInDevice + secondsQueuedInOpenAL) * 1'000));

	return countExact
		? queuedTotal
		: std::max(queuedTotal, kDefaultPlayoutLatency);
}

void AudioDeviceOpenAL::processPlayoutData() {
	Expects(_data != nullptr);

	const auto playing = [&] {
		auto state = ALint(AL_INITIAL);
		alGetSourcei(_data->source, AL_SOURCE_STATE, &state);
		return (state == AL_PLAYING);
	};
	const auto wasPlaying = playing();

	if (wasPlaying) {
		clearProcessedBuffers();
	} else {
		unqueueAllBuffers();
	}

	const auto wereQueued = _data->queuedBuffers;
	while (_data->queuedBuffersCount < kBuffersKeepReadyCount) {
		const auto available = _audioDeviceBuffer.RequestPlayoutData(
			kPlayoutPart);
		if (available == kPlayoutPart) {
			_audioDeviceBuffer.GetPlayoutData(_data->playoutSamples.data());
		} else {
			//ranges::fill(_data->playoutSamples, 0);
			break;
		}
		const auto now = crl::now();
		_playoutLatency = countExactQueuedMsForLatency(now, wasPlaying);
		//RTC_LOG(LS_ERROR) << "PLAYOUT LATENCY: " << _playoutLatency << "ms";

		const auto i = ranges::find(_data->queuedBuffers, false);
		Assert(i != end(_data->queuedBuffers));
		const auto index = int(i - begin(_data->queuedBuffers));
		alBufferData(
			_data->buffers[index],
			(_playoutChannels == 2) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16,
			_data->playoutSamples.data(),
			_data->playoutSamples.size(),
			kPlayoutFrequency);

#ifdef WEBRTC_WIN
		if (IsLoopbackCaptureActive() && _playoutChannels == 2) {
			LoopbackCapturePushFarEnd(
				now + _playoutLatency,
				_data->playoutSamples,
				kPlayoutFrequency,
				_playoutChannels);
		}
#endif // WEBRTC_WIN

		_data->queuedBuffers[index] = true;
		++_data->queuedBuffersCount;
		if (wasPlaying) {
			alSourceQueueBuffers(
				_data->source,
				1,
				_data->buffers.data() + index);
		}
	}
	if (!_data->queuedBuffersCount) {
		return;
	}
	if (!playing()) {
		if (wasPlaying) {
			// While we were queueing buffers the source stopped.
			// Now we can't unqueue only old buffers, so we unqueue all
			// of them and then re-queue the ones we queued right now.
			unqueueAllBuffers();
			for (auto i = 0; i != int(_data->buffers.size()); ++i) {
				if (!wereQueued[i] && _data->queuedBuffers[i]) {
					alSourceQueueBuffers(
						_data->source,
						1,
						_data->buffers.data() + i);
				}
			}
		} else {
			// We were not playing and had no buffers,
			// so queue them all at once.
			alSourceQueueBuffers(
				_data->source,
				_data->queuedBuffersCount,
				_data->buffers.data());
		}
		alSourcePlay(_data->source);
	}

	if (Failed(_playoutDevice)) {
		_playoutFailed = true;
	}
}

int32_t AudioDeviceOpenAL::StartRecording() {
	if (!_recordingInitialized) {
		return -1;
	} else if (_data && _data->recording) {
		return 0;
	}
	if (_recordingFailed) {
		_recordingFailed = false;
		openRecordingDevice();
	}
	_audioDeviceBuffer.StartRecording();
	startCaptureOnThread();
	return 0;
}

void AudioDeviceOpenAL::startCaptureOnThread() {
	Expects(_data != nullptr);

	sync([&] {
		_data->recording = true;
		if (_recordingFailed) {
			return;
		}
		alcCaptureStart(_recordingDevice);
		if (Failed(_recordingDevice)) {
			_recordingFailed = true;
			return;
		}
		if (!_data->timer.isActive()) {
			_data->timer.callEach(kProcessInterval);
		}
	});
	if (_recordingFailed) {
		closeRecordingDevice();
	}
}

void AudioDeviceOpenAL::stopCaptureOnThread() {
	Expects(_data != nullptr);

	if (!_data->recording) {
		return;
	}
	sync([&] {
		_data->recording = false;
		if (_recordingFailed) {
			return;
		}
		if (!_data->playing) {
			_data->timer.cancel();
		}
		if (_recordingDevice) {
			alcCaptureStop(_recordingDevice);
		}
	});
}

void AudioDeviceOpenAL::startPlayingOnThread() {
	Expects(_data != nullptr);

	sync([&] {
		_data->playing = true;
		if (_playoutFailed) {
			return;
		}
		ALuint source = 0;
		alGenSources(1, &source);
		if (source) {
			alSourcef(source, AL_PITCH, 1.f);
			alSource3f(source, AL_POSITION, 0, 0, 0);
			alSource3f(source, AL_VELOCITY, 0, 0, 0);
			alSourcei(source, AL_LOOPING, 0);
			alSourcei(source, AL_SOURCE_RELATIVE, 1);
			alSourcei(source, AL_ROLLOFF_FACTOR, 0);
			if (alIsExtensionPresent("AL_SOFT_direct_channels_remix")) {
				alSourcei(
					source,
					alGetEnumValue("AL_DIRECT_CHANNELS_SOFT"),
					alGetEnumValue("AL_REMIX_UNMATCHED_SOFT"));
			}
			_data->source = source;
			alGenBuffers(_data->buffers.size(), _data->buffers.data());

			_data->exactDeviceTimeCounter = 0;
			_data->lastExactDeviceTime = 0;
			_data->lastExactDeviceTimeWhen = 0;

			const auto bufferSize = kPlayoutPart * sizeof(int16_t)
				* _playoutChannels;

			_data->playoutSamples = QByteArray(bufferSize, 0);
			//for (auto i = 0; i != kBuffersKeepReadyCount; ++i) {
			//	alBufferData(
			//		_data->buffers[i],
			//		AL_FORMAT_STEREO16,
			//		_data->playoutSamples.data(),
			//		_data->playoutSamples.size(),
			//		kPlayoutFrequency);
			//	_data->queuedBuffers[i] = true;
			//}
			//_data->queuedBuffersCount = kBuffersKeepReadyCount;
			//alSourceQueueBuffers(
			//	source,
			//	kBuffersKeepReadyCount,
			//	_data->buffers.data());
			//alSourcePlay(source);

			if (!_data->timer.isActive()) {
				_data->timer.callEach(kProcessInterval);
			}
		}
	});
}

void AudioDeviceOpenAL::stopPlayingOnThread() {
	Expects(_data != nullptr);

	sync([&] {
		const auto guard = gsl::finally([&] {
			if (alEventCallbackSOFT) {
				alEventCallbackSOFT(nullptr, nullptr);
			}
			alcSetThreadContext(nullptr);
		});
		if (!_data->playing) {
			return;
		}
		_data->playing = false;
		if (_playoutFailed) {
			return;
		}
		if (!_data->recording) {
			_data->timer.cancel();
		}
		if (_data->source) {
			alSourceStop(_data->source);
			unqueueAllBuffers();
			alDeleteBuffers(_data->buffers.size(), _data->buffers.data());
			alDeleteSources(1, &_data->source);
			_data->source = 0;
			ranges::fill(_data->buffers, ALuint(0));
		}
	});
}

int32_t AudioDeviceOpenAL::StopRecording() {
	if (_data) {
		stopCaptureOnThread();
		_audioDeviceBuffer.StopRecording();
		if (!_data->playing) {
			_data->thread.quit();
			_data->thread.wait();
			_data = nullptr;
		}
	}
	closeRecordingDevice();
	_recordingInitialized = false;
	return 0;
}

void AudioDeviceOpenAL::restartRecordingQueued() {
	Expects(_data != nullptr);

	if (!_thread) {
		// We support auto-restarting only when started from rtc::Thread.
		return;
	}
	const auto weak = QPointer<QObject>(&_data->context);
	_thread->PostTask([=] {
		if (weak) {
			restartRecording();
			InvokeQueued(&_data->context, [=] {
				_data->emptyRecordingData = 0;
			});
		}
	});
}

int AudioDeviceOpenAL::restartRecording() {
	if (!_data || !_data->recording) {
		return 0;
	}
	stopCaptureOnThread();
	closeRecordingDevice();

	_recordingFailed = false;
	openRecordingDevice();
	startCaptureOnThread();
	return 0;
}

void AudioDeviceOpenAL::restartPlayoutQueued() {
	Expects(_data != nullptr);

	if (!_thread) {
		// We support auto-restarting only when started from rtc::Thread.
		return;
	}
	const auto weak = QPointer<QObject>(&_data->context);
	_thread->PostTask([=] {
		if (weak) {
			restartPlayout();
		}
	});
}

int AudioDeviceOpenAL::restartPlayout() {
	if (!_data || !_data->playing) {
		return 0;
	}
	stopPlayingOnThread();
	closePlayoutDevice();

	_playoutFailed = false;
	openPlayoutDevice();
	startPlayingOnThread();
	return 0;
}

void AudioDeviceOpenAL::closeRecordingDevice() {
	//if (_context) {
	//	alcDestroyContext(_context);
	//	_context = nullptr;
	//}
	if (_recordingDevice) {
		alcCaptureCloseDevice(_recordingDevice);
		_recordingDevice = nullptr;
	}
}

void AudioDeviceOpenAL::closePlayoutDevice() {
	if (_playoutContext) {
		alcDestroyContext(_playoutContext);
		_playoutContext = nullptr;
	}
	if (_playoutDevice) {
		alcCloseDevice(_playoutDevice);
		_playoutDevice = nullptr;
	}
}

bool AudioDeviceOpenAL::RecordingIsInitialized() const {
	return _recordingInitialized;
}

bool AudioDeviceOpenAL::Recording() const {
	return _data && _data->recording;
}

bool AudioDeviceOpenAL::PlayoutIsInitialized() const {
	return _playoutInitialized;
}

int32_t AudioDeviceOpenAL::StartPlayout() {
	if (!_playoutInitialized) {
		return -1;
	} else if (Playing()) {
		return 0;
	}
	if (_playoutFailed) {
		_playoutFailed = false;
		openPlayoutDevice();
	}
	_audioDeviceBuffer.SetPlayoutSampleRate(kPlayoutFrequency);
	_audioDeviceBuffer.SetPlayoutChannels(_playoutChannels);
	_audioDeviceBuffer.StartPlayout();
	startPlayingOnThread();
	return 0;
}

int32_t AudioDeviceOpenAL::StopPlayout() {
	if (_data) {
		stopPlayingOnThread();
		_audioDeviceBuffer.StopPlayout();
		if (!_data->recording) {
			_data->thread.quit();
			_data->thread.wait();
			_data = nullptr;
		}
	}
	closePlayoutDevice();
	_playoutInitialized = false;
	return 0;
}

int32_t AudioDeviceOpenAL::PlayoutDelay(uint16_t *delayMS) const {
	if (delayMS) {
		*delayMS = 0;
	}
	return 0;
}

bool AudioDeviceOpenAL::BuiltInAECIsAvailable() const {
	return false;
}

bool AudioDeviceOpenAL::BuiltInAGCIsAvailable() const {
	return false;
}

bool AudioDeviceOpenAL::BuiltInNSIsAvailable() const {
	return false;
}

int32_t AudioDeviceOpenAL::EnableBuiltInAEC(bool enable) {
	return enable ? -1 : 0;
}

int32_t AudioDeviceOpenAL::EnableBuiltInAGC(bool enable) {
	return enable ? -1 : 0;
}

int32_t AudioDeviceOpenAL::EnableBuiltInNS(bool enable) {
	return enable ? -1 : 0;
}

bool AudioDeviceOpenAL::Playing() const {
	return _data && _data->playing;
}

} // namespace Webrtc::details
