// Copyright 2007-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "AudioConfigDialog.h"

#include "Accessibility.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "AudioOutputSample.h"
#include "AudioOutputToken.h"
#include "NetworkConfig.h"
#include "Utils.h"
#include "Global.h"

#include <QSignalBlocker>

#include <cstdint>

const QString AudioOutputDialog::name = QLatin1String("AudioOutputWidget");
const QString AudioInputDialog::name  = QLatin1String("AudioInputWidget");


static ConfigWidget *AudioInputDialogNew(Settings &st) {
	return new AudioInputDialog(st);
}

static ConfigWidget *AudioOutputDialogNew(Settings &st) {
	return new AudioOutputDialog(st);
}

static ConfigRegistrar registrarAudioInputDialog(1000, AudioInputDialogNew);
static ConfigRegistrar registrarAudioOutputDialog(1010, AudioOutputDialogNew);

void AudioInputDialog::hideEvent(QHideEvent *) {
	qtTick->stop();
}

void AudioInputDialog::showEvent(QShowEvent *) {
	qtTick->start(20);

	// In case the user has changed the audio output device and is now coming
	// back.
	updateEchoEnableState();
}

AudioInputDialog::AudioInputDialog(Settings &st) : ConfigWidget(st) {
	qtTick = new QTimer(this);
	qtTick->setObjectName(QLatin1String("Tick"));

	setupUi(this);

	qlInputHelp->setVisible(false);

	if (AudioInputRegistrar::qmNew) {
		QList< QString > keys = AudioInputRegistrar::qmNew->keys();
		foreach (QString key, keys) { qcbSystem->addItem(key); }
	}
	qcbSystem->setEnabled(qcbSystem->count() > 1);

	qcbTransmit->addItem(tr("Continuous"), Settings::Continuous);
	qcbTransmit->addItem(tr("Voice Activity"), Settings::VAD);
	qcbTransmit->addItem(tr("Push To Talk"), Settings::PushToTalk);

	abSpeech->qcBelow  = Qt::red;
	abSpeech->qcInside = Qt::yellow;
	abSpeech->qcAbove  = Qt::green;

	qcbDevice->view()->setTextElideMode(Qt::ElideRight);

	on_qcbMuteCue_clicked(Global::get().s.bTxMuteCue);
	on_Tick_timeout();
	on_qcbIdleAction_currentIndexChanged(Global::get().s.iaeIdleAction);

	// Hide the slider by default
	showSpeexNoiseSuppressionSlider(false);

#ifndef USE_RENAMENOISE
	// Hide options related to ReNameNoise
	qrbNoiseSupReNameNoise->setVisible(false);
	qrbNoiseSupBoth->setVisible(false);
#endif
}

QString AudioInputDialog::title() const {
	return tr("Audio Input");
}

const QString &AudioInputDialog::getName() const {
	return AudioInputDialog::name;
}

QIcon AudioInputDialog::icon() const {
	return QIcon(QLatin1String("skin:config_audio_input.png"));
}

void AudioInputDialog::load(const Settings &r) {
	int i;
	QList< QString > keys;

	if (AudioInputRegistrar::qmNew)
		keys = AudioInputRegistrar::qmNew->keys();
	else
		keys.clear();
	i = keys.indexOf(AudioInputRegistrar::current);
	if (i >= 0)
		loadComboBox(qcbSystem, i);

	verifyMicrophonePermission();

	loadCheckBox(qcbExclusive, r.bExclusiveInput);

	qlePushClickPathOn->setText(r.qsTxAudioCueOn);
	qlePushClickPathOff->setText(r.qsTxAudioCueOff);
	qleMuteCuePath->setText(r.qsTxMuteCue);
	s.muteCueShown = r.muteCueShown;


	loadComboBox(qcbTransmit, r.atTransmit);
	loadSlider(qsTransmitHold, r.iVoiceHold);
	loadSlider(qsTransmitMin, static_cast< int >(r.fVADmin * 32767.0f + 0.5f));
	loadSlider(qsTransmitMax, static_cast< int >(r.fVADmax * 32767.0f + 0.5f));
	loadSlider(qsFrames, (r.iFramesPerPacket == 1) ? 1 : (r.iFramesPerPacket / 2 + 1));
	loadSlider(qsDoublePush, static_cast< int >(static_cast< float >(r.uiDoublePush) / 1000.f + 0.5f));
	loadSlider(qsPTTHold, static_cast< int >(r.pttHold));

	if (r.vsVAD == Settings::Amplitude)
		qrbAmplitude->setChecked(true);
	else
		qrbSNR->setChecked(true);

	loadCheckBox(qcbPushWindow, r.bShowPTTButtonWindow);
	loadCheckBox(qcbEnableCuePTT, r.audioCueEnabledPTT);
	loadCheckBox(qcbEnableCueVAD, r.audioCueEnabledVAD);
	updateAudioCueEnabled();
	loadCheckBox(qcbMuteCue, r.bTxMuteCue);
	loadSlider(qsQuality, r.iQuality);
	loadCheckBox(qcbAllowLowDelay, r.bAllowLowDelay);
	if (r.iSpeexNoiseCancelStrength != 0) {
		loadSlider(qsSpeexNoiseSupStrength, -r.iSpeexNoiseCancelStrength);
	} else {
		loadSlider(qsSpeexNoiseSupStrength, 14);
	}

	bool allowReNameNoise = SAMPLE_RATE == 48000;

	if (!allowReNameNoise) {
		const QString tooltip = QObject::tr("RNNoise is not available due to a sample rate mismatch.");
		qrbNoiseSupReNameNoise->setEnabled(false);
		qrbNoiseSupReNameNoise->setToolTip(tooltip);
		qrbNoiseSupBoth->setEnabled(false);
		qrbNoiseSupBoth->setToolTip(tooltip);
	}

	switch (r.noiseCancelMode) {
		case Settings::NoiseCancelOff:
			loadCheckBox(qrbNoiseSupDeactivated, true);
			break;
		case Settings::NoiseCancelSpeex:
			loadCheckBox(qrbNoiseSupSpeex, true);
			break;
		case Settings::NoiseCancelRNN:
#ifdef USE_RENAMENOISE
			if (allowReNameNoise) {
				loadCheckBox(qrbNoiseSupReNameNoise, true);
			} else {
				// We have to switch to speex as a fallback
				loadCheckBox(qrbNoiseSupSpeex, true);
			}
#else
			// We have to switch to speex as a fallback
			loadCheckBox(qrbNoiseSupSpeex, true);
#endif
			break;
		case Settings::NoiseCancelBoth:
#ifdef USE_RENAMENOISE
			if (allowReNameNoise) {
				loadCheckBox(qrbNoiseSupBoth, true);
			} else {
				// We have to switch to speex as a fallback
				loadCheckBox(qrbNoiseSupSpeex, true);
			}
#else
			// We have to switch to speex as a fallback
			loadCheckBox(qrbNoiseSupSpeex, true);
#endif
			break;
	}

	loadSlider(qsAmp, 20000 - r.iMinLoudness);

	// Idle auto actions
	qsbIdle->setValue(static_cast< int >(r.iIdleTime) / 60);
	loadComboBox(qcbIdleAction, r.iaeIdleAction);
	loadCheckBox(qcbUndoIdleAction, r.bUndoIdleActionUponActivity);

	updateEchoEnableState();
}

void AudioInputDialog::verifyMicrophonePermission() {
	if (!AudioInputRegistrar::qmNew) {
		return;
	}

	AudioInputRegistrar *air = AudioInputRegistrar::qmNew->value(qcbSystem->currentText());
	if (air->isMicrophoneAccessDeniedByOS()) {
		qcbDevice->setEnabled(false);
		if (air->name == QLatin1String("CoreAudio")) {
			qlInputHelp->setVisible(true);
			qlInputHelp->setText(
				tr("Access to the microphone was denied. Please allow Mumble to use the microphone "
				   "by changing the settings in System Preferences -> Security & Privacy -> Privacy -> "
				   "Microphone."));
		} else if (air->name == QLatin1String("WASAPI")) {
			qlInputHelp->setVisible(true);
			qlInputHelp->setText(tr("Access to the microphone was denied. Please check that your operating system's "
									"microphone settings allow Mumble to use the microphone."));
		}
	} else {
		qcbDevice->setEnabled(true);
		qlInputHelp->setVisible(false);
		qlInputHelp->setText("");
	}
}

void AudioInputDialog::save() const {
	s.iQuality                  = qsQuality->value();
	s.bAllowLowDelay            = qcbAllowLowDelay->isChecked();
	s.iSpeexNoiseCancelStrength = (qsSpeexNoiseSupStrength->value() == 14) ? 0 : -qsSpeexNoiseSupStrength->value();

	if (qrbNoiseSupDeactivated->isChecked()) {
		s.noiseCancelMode = Settings::NoiseCancelOff;
	} else if (qrbNoiseSupBoth->isChecked()) {
		s.noiseCancelMode = Settings::NoiseCancelBoth;
	} else if (qrbNoiseSupReNameNoise->isChecked()) {
		s.noiseCancelMode = Settings::NoiseCancelRNN;
	} else {
		s.noiseCancelMode = Settings::NoiseCancelSpeex;
	}

	s.iMinLoudness     = 18000 - qsAmp->value() + 2000;
	s.iVoiceHold       = qsTransmitHold->value();
	s.fVADmin          = static_cast< float >(qsTransmitMin->value()) / 32767.0f;
	s.fVADmax          = static_cast< float >(qsTransmitMax->value()) / 32767.0f;
	s.vsVAD            = qrbSNR->isChecked() ? Settings::SignalToNoise : Settings::Amplitude;
	s.iFramesPerPacket = qsFrames->value();
	s.iFramesPerPacket = (s.iFramesPerPacket == 1) ? 1 : ((s.iFramesPerPacket - 1) * 2);
	s.uiDoublePush     = static_cast< unsigned int >(qsDoublePush->value() * 1000);
	s.pttHold          = static_cast< quint64 >(qsPTTHold->value());
	s.atTransmit       = static_cast< Settings::AudioTransmit >(qcbTransmit->currentIndex());

	// Idle auto actions
	s.iIdleTime                   = static_cast< unsigned int >(qsbIdle->value() * 60);
	s.iaeIdleAction               = static_cast< Settings::IdleAction >(qcbIdleAction->currentIndex());
	s.bUndoIdleActionUponActivity = qcbUndoIdleAction->isChecked();

	s.bShowPTTButtonWindow = qcbPushWindow->isChecked();
	s.audioCueEnabledPTT   = qcbEnableCuePTT->isChecked();
	s.audioCueEnabledVAD   = qcbEnableCueVAD->isChecked();
	s.qsTxAudioCueOn       = qlePushClickPathOn->text();
	s.qsTxAudioCueOff      = qlePushClickPathOff->text();

	s.bTxMuteCue  = qcbMuteCue->isChecked();
	s.qsTxMuteCue = qleMuteCuePath->text();

	s.qsAudioInput    = qcbSystem->currentText();
	s.echoOption      = static_cast< EchoCancelOptionID >(qcbEcho->currentData().toInt());
	s.bExclusiveInput = qcbExclusive->isChecked();

	if (AudioInputRegistrar::qmNew) {
		AudioInputRegistrar *air = AudioInputRegistrar::qmNew->value(qcbSystem->currentText());
		int idx                  = qcbDevice->currentIndex();
		if (idx > -1) {
			air->setDeviceChoice(qcbDevice->itemData(idx), s);
		}
	}
}

void AudioInputDialog::on_qsFrames_valueChanged(int v) {
	int val = (v == 1) ? 10 : (v - 1) * 20;
	qlFrames->setText(tr("%1 ms").arg(val));
	updateBitrate();

	Mumble::Accessibility::setSliderSemanticValue(qsFrames, QString("%1 %2").arg(val).arg(tr("milliseconds")));
}

void AudioInputDialog::on_qsDoublePush_valueChanged(int v) {
	if (v == 0) {
		qlDoublePush->setText(tr("Off"));
		Mumble::Accessibility::setSliderSemanticValue(qsDoublePush, tr("Off"));
	} else {
		qlDoublePush->setText(tr("%1 ms").arg(v));
		Mumble::Accessibility::setSliderSemanticValue(qsDoublePush, QString("%1 %2").arg(v).arg(tr("milliseconds")));
	}
}

void AudioInputDialog::on_qsPTTHold_valueChanged(int v) {
	if (v == 0) {
		qlPTTHold->setText(tr("Off"));
		Mumble::Accessibility::setSliderSemanticValue(qsPTTHold, tr("Off"));
	} else {
		qlPTTHold->setText(tr("%1 ms").arg(v));
		Mumble::Accessibility::setSliderSemanticValue(qsPTTHold, QString("%1 %2").arg(v).arg(tr("milliseconds")));
	}
}

void AudioInputDialog::on_qsTransmitHold_valueChanged(int v) {
	float val = static_cast< float >(v * 10);
	val       = val / 1000.0f;
	qlTransmitHold->setText(tr("%1 s").arg(val, 0, 'f', 2));
	Mumble::Accessibility::setSliderSemanticValue(qsTransmitHold,
												  QString("%1 %2").arg(val, 0, 'f', 2).arg(tr("seconds")));
}

void AudioInputDialog::on_qsQuality_valueChanged(int v) {
	qlQuality->setText(tr("%1 kb/s").arg(static_cast< float >(v) / 1000.0f, 0, 'f', 1));
	updateBitrate();

	Mumble::Accessibility::setSliderSemanticValue(
		qsQuality, QString("%1 %2").arg(static_cast< float >(v) / 1000.0f, 0, 'f', 1).arg(tr("kilobits per second")));
}

void AudioInputDialog::on_qsSpeexNoiseSupStrength_valueChanged(int v) {
	QPalette pal;

	if (v < 15) {
		qlSpeexNoiseSupStrength->setText(tr("Off"));
		pal.setColor(qlSpeexNoiseSupStrength->foregroundRole(), Qt::red);
		Mumble::Accessibility::setSliderSemanticValue(qsSpeexNoiseSupStrength, tr("Off"));
	} else {
		qlSpeexNoiseSupStrength->setText(tr("-%1 dB").arg(v));
		Mumble::Accessibility::setSliderSemanticValue(qsSpeexNoiseSupStrength,
													  QString("-%1 %2").arg(v).arg(tr("decibels")));
	}
	qlSpeexNoiseSupStrength->setPalette(pal);
}

void AudioInputDialog::on_qsAmp_valueChanged(int v) {
	v       = 18000 - v + 2000;
	float d = 20000.0f / static_cast< float >(v);
	qlAmp->setText(QString::fromLatin1("%1").arg(d, 0, 'f', 2));

	Mumble::Accessibility::setSliderSemanticValue(qsAmp, QString("%1").arg(d, 0, 'f', 2));
}

void AudioInputDialog::on_qsTransmitMin_valueChanged() {
	Mumble::Accessibility::setSliderSemanticValue(qsTransmitMin, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioInputDialog::on_qsTransmitMax_valueChanged() {
	Mumble::Accessibility::setSliderSemanticValue(qsTransmitMax, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioInputDialog::updateBitrate() {
	if (!qsQuality || !qsFrames || !qlBitrate)
		return;
	int q = qsQuality->value();
	int p = qsFrames->value();

	int audiorate, overhead, posrate;

	audiorate = q;

	// 50 packets, in bits, IP + UDP + Crypt + type + seq + frameheader
	overhead = 100 * 8 * (20 + 8 + 4 + 1 + 2 + p);

	// TCP is 12 more bytes than UDP
	if (NetworkConfig::TcpModeEnabled())
		overhead += 100 * 8 * 12;

	if (Global::get().s.bTransmitPosition)
		posrate = 12;
	else
		posrate = 0;

	posrate = posrate * 100 * 8;

	overhead = overhead / p;
	posrate  = posrate / p;

	int total = audiorate + overhead + posrate;

	QPalette pal;

	if (Global::get().uiSession && (total > Global::get().iMaxBandwidth)) {
		pal.setColor(qlBitrate->foregroundRole(), Qt::red);
	}

	qlBitrate->setPalette(pal);

	QString v = tr("%1 kbit/s (Audio %2, Position %4, Overhead %3)")
					.arg(total / 1000.0, 0, 'f', 1)
					.arg(audiorate / 1000.0, 0, 'f', 1)
					.arg(overhead / 1000.0, 0, 'f', 1)
					.arg(posrate / 1000.0, 0, 'f', 1);

	qlBitrate->setText(v);

	qsQuality->setMinimum(8000);

	qsQuality->setAccessibleDescription(v);
	qsFrames->setAccessibleDescription(v);
}

void AudioInputDialog::on_qcbEnableCuePTT_clicked() {
	updateAudioCueEnabled();
}

void AudioInputDialog::on_qcbEnableCueVAD_clicked() {
	updateAudioCueEnabled();
}

void AudioInputDialog::updateAudioCueEnabled() {
	bool enabled = qcbEnableCuePTT->isChecked() || qcbEnableCueVAD->isChecked();

	qpbPushClickBrowseOn->setEnabled(enabled);
	qpbPushClickBrowseOff->setEnabled(enabled);
	qpbPushClickPreview->setEnabled(enabled);
	qpbPushClickReset->setEnabled(enabled);
	qlePushClickPathOn->setEnabled(enabled);
	qlePushClickPathOff->setEnabled(enabled);
	qlPushClickOn->setEnabled(enabled);
	qlPushClickOff->setEnabled(enabled);
}

void AudioInputDialog::on_qpbPushClickBrowseOn_clicked() {
	QString defaultpath(qlePushClickPathOn->text());
	QString qsnew = AudioOutputSample::browseForSndfile(defaultpath);
	if (!qsnew.isEmpty())
		qlePushClickPathOn->setText(qsnew);
}

void AudioInputDialog::on_qpbPushClickBrowseOff_clicked() {
	QString defaultpath(qlePushClickPathOff->text());
	QString qsnew = AudioOutputSample::browseForSndfile(defaultpath);
	if (!qsnew.isEmpty())
		qlePushClickPathOff->setText(qsnew);
}

void AudioInputDialog::on_qpbPushClickPreview_clicked() {
	AudioOutputPtr ao = Global::get().ao;
	if (ao) {
		AudioOutputToken sample = ao->playSample(qlePushClickPathOn->text(), Global::get().s.cueVolume);
		if (sample) {
			sample.connect< AudioOutputSample >(&AudioOutputSample::playbackFinished, *this,
												&AudioInputDialog::continuePlayback);
		} else {
			// If we fail to playback the first play on play at least off
			ao->playSample(qlePushClickPathOff->text(), Global::get().s.cueVolume);
		}
	}
}

void AudioInputDialog::on_qcbMuteCue_clicked(bool b) {
	qleMuteCuePath->setEnabled(b);
	qpbMuteCueBrowse->setEnabled(b);
	qpbMuteCuePreview->setEnabled(b);
	s.muteCueShown = true;
}

void AudioInputDialog::on_qpbMuteCueBrowse_clicked() {
	QString defaultpath(qleMuteCuePath->text());
	QString qsnew = AudioOutputSample::browseForSndfile(defaultpath);
	if (!qsnew.isEmpty())
		qleMuteCuePath->setText(qsnew);
}


void AudioInputDialog::on_qpbMuteCuePreview_clicked() {
	AudioOutputPtr ao = Global::get().ao;
	if (ao) {
		ao->playSample(qleMuteCuePath->text(), Global::get().s.cueVolume);
	}
}

void AudioInputDialog::continuePlayback() {
	AudioOutputPtr ao = Global::get().ao;
	if (ao) {
		ao->playSample(qlePushClickPathOff->text(), Global::get().s.cueVolume);
	}
}

void AudioInputDialog::on_qpbPushClickReset_clicked() {
	qlePushClickPathOn->setText(Settings::cqsDefaultPushClickOn);
	qlePushClickPathOff->setText(Settings::cqsDefaultPushClickOff);
}

void AudioInputDialog::on_qcbTransmit_currentIndexChanged(int v) {
	switch (v) {
		case 0:
			qswTransmit->setCurrentWidget(qwContinuous);
			break;
		case 1:
			qswTransmit->setCurrentWidget(qwVAD);
			break;
		case 2:
			qswTransmit->setCurrentWidget(qwPTT);
			break;
	}
}

void AudioInputDialog::on_qcbSystem_currentIndexChanged(int) {
	qcbDevice->clear();

	QList< audioDevice > choices;

	if (AudioInputRegistrar::qmNew) {
		auto air         = AudioInputRegistrar::qmNew->value(qcbSystem->currentText());
		QVariant current = air->getDeviceChoice();
		choices          = air->getDeviceChoices();

		for (int i = 0; i < choices.size(); ++i) {
			auto &choice = choices.at(i);
			qcbDevice->addItem(choice.first, choice.second);
			qcbDevice->setItemData(i, choice.first.toHtmlEscaped(), Qt::ToolTipRole);

			if (choice.second == current) {
				qcbDevice->setCurrentIndex(i);
			}
		}

		updateEchoEnableState();

		qcbExclusive->setEnabled(air->canExclusive());
	}

	qcbDevice->setEnabled(!choices.isEmpty());
	verifyMicrophonePermission();
}

void AudioInputDialog::updateEchoEnableState() {
	AudioInputRegistrar *air = AudioInputRegistrar::qmNew->value(qcbSystem->currentText());

	AudioOutputDialog *outputWidget =
		static_cast< AudioOutputDialog * >(ConfigDialog::getConfigWidget(AudioOutputDialog::name));
	QString outputInterface;
	if (outputWidget) {
		outputInterface = outputWidget->getCurrentlySelectedOutputInterfaceName();
	} else {
		// Fallback for when the outputWIdget is not (yet) available -> use the value from the current settings
		outputInterface = s.qsAudioOutput;
	}

	qcbEcho->clear();
	qcbEcho->setToolTip(QObject::tr("If enabled this tries to cancel out echo from the audio stream."));
	qcbEcho->setCurrentIndex(0);

	bool hasUsableEchoOption = false;

	qcbEcho->insertItem(0, tr("Disabled"), "disabled");
	qcbEcho->setItemData(0, tr("Disable echo cancellation."), Qt::ToolTipRole);

	int i = 0;
	for (EchoCancelOptionID echoid : air->echoOptions) {
		if (air->canEcho(echoid, outputInterface)) {
			++i;
			hasUsableEchoOption                = true;
			const EchoCancelOption &echoOption = EchoCancelOption::getOptions()[static_cast< std::size_t >(echoid)];
			qcbEcho->insertItem(i, echoOption.description, static_cast< int >(echoid));
			qcbEcho->setItemData(i, echoOption.explanation, Qt::ToolTipRole);
			if (s.echoOption == echoid) {
				qcbEcho->setCurrentIndex(i);
			}
		}
	}

	if (hasUsableEchoOption) {
		qcbEcho->setEnabled(true);
	} else {
		qcbEcho->setCurrentIndex(0);
		qcbEcho->setEnabled(false);
		qcbEcho->setToolTip(QObject::tr("Echo cancellation is not supported for the interface "
										"combination \"%1\" (in) and \"%2\" (out).")
								.arg(air->name)
								.arg(outputInterface));
	}
}

void AudioInputDialog::showSpeexNoiseSuppressionSlider(bool show) {
	qlSpeexNoiseSup->setVisible(show);
	qsSpeexNoiseSupStrength->setVisible(show);
	qlSpeexNoiseSupStrength->setVisible(show);
}

void AudioInputDialog::on_Tick_timeout() {
	AudioInputPtr ai = Global::get().ai;

	if (!ai.get() || !ai->sppPreprocess)
		return;

	abSpeech->iBelow = qsTransmitMin->value();
	abSpeech->iAbove = qsTransmitMax->value();

	if (qrbAmplitude->isChecked()) {
		abSpeech->iValue = static_cast< int >((32767.f / 96.0f) * (96.0f + ai->dPeakCleanMic) + 0.5f);
	} else {
		abSpeech->iValue = static_cast< int >(ai->fSpeechProb * 32767.0f + 0.5f);
	}
	abSpeech->update();
}


void AudioInputDialog::on_qcbIdleAction_currentIndexChanged(int v) {
	const Settings::IdleAction action = static_cast< Settings::IdleAction >(v);

	const bool enabled = (action != Settings::Nothing);

	qlIdle->setEnabled(enabled);
	qlIdle2->setEnabled(enabled);
	qsbIdle->setEnabled(enabled);
	qcbUndoIdleAction->setEnabled(enabled);
}

void AudioInputDialog::on_qrbNoiseSupSpeex_toggled(bool checked) {
	showSpeexNoiseSuppressionSlider(checked);
}

void AudioInputDialog::on_qrbNoiseSupBoth_toggled(bool checked) {
	showSpeexNoiseSuppressionSlider(checked);
}

void AudioOutputDialog::enablePulseAudioAttenuationOptionsFor(const QString &outputName) {
	if (outputName == QLatin1String("PulseAudio")) {
		qcbOnlyAttenuateSameOutput->show();
		qcbAttenuateLoopbacks->show();
	} else {
		qcbOnlyAttenuateSameOutput->hide();
		qcbAttenuateLoopbacks->hide();
	}
}

AudioOutputDialog::AudioOutputDialog(Settings &st) : ConfigWidget(st) {
	setupUi(this);

	if (AudioOutputRegistrar::qmNew) {
		QList< QString > keys = AudioOutputRegistrar::qmNew->keys();
		foreach (QString key, keys) { qcbSystem->addItem(key); }
	}
	qcbSystem->setEnabled(qcbSystem->count() > 1);

	qcbLoopback->addItem(tr("None"), Settings::None);
	qcbLoopback->addItem(tr("Local"), Settings::Local);
	qcbLoopback->addItem(tr("Server"), Settings::Server);

	qcbDevice->view()->setTextElideMode(Qt::ElideRight);

	// Distance in cm
	qsMinDistance->setRange(0, 200);
	// Distance in m
	qsbMinimumDistance->setRange(0.0, 1000.0);
	// Distance in cm
	qsMaxDistance->setRange(10, 2000);
	// Distance in m
	qsbMaximumDistance->setRange(1.0, 10000.0);
	qsMinimumVolume->setRange(0, 100);
	qsbMinimumVolume->setRange(qsMinimumVolume->minimum(), qsMinimumVolume->maximum());
	qsBloom->setRange(0, 75);
	qsbBloom->setRange(qsBloom->minimum(), qsBloom->maximum());

	QString minDistanceTooltip = tr("Distance at which audio volume from another player starts decreasing");
	QString maxDistanceTooltip = tr("Distance at which a player's audio volume has reached its minimum value");
	QString minVolumeTooltip =
		tr("The minimum volume a player's audio will fade out to with increasing distance. Set to 0% for it to fade "
		   "into complete silence for a realistic maximum hearing distance.");
	QString bloomTooltip = tr("If an audio source is close enough, blooming will cause the audio to be played on all "
							  "speakers more or less regardless of their position (albeit with lower volume)");

	qlMinDistance->setToolTip(minDistanceTooltip);
	qsMinDistance->setToolTip(minDistanceTooltip);
	qsbMinimumDistance->setToolTip(minDistanceTooltip);
	qlMaxDistance->setToolTip(maxDistanceTooltip);
	qsMaxDistance->setToolTip(maxDistanceTooltip);
	qsbMaximumDistance->setToolTip(maxDistanceTooltip);
	qlMinimumVolume->setToolTip(minVolumeTooltip);
	qsMinimumVolume->setToolTip(minVolumeTooltip);
	qsbMinimumVolume->setToolTip(minVolumeTooltip);
	qlBloom->setToolTip(bloomTooltip);
	qsBloom->setToolTip(bloomTooltip);
	qsbBloom->setToolTip(bloomTooltip);
}

QString AudioOutputDialog::title() const {
	return tr("Audio Output");
}

const QString &AudioOutputDialog::getName() const {
	return AudioOutputDialog::name;
}

QIcon AudioOutputDialog::icon() const {
	return QIcon(QLatin1String("skin:config_audio_output.png"));
}

QString AudioOutputDialog::getCurrentlySelectedOutputInterfaceName() const {
	if (qcbSystem) {
		return qcbSystem->currentText();
	}

	return QString();
}

void AudioOutputDialog::load(const Settings &r) {
	int i;
	QList< QString > keys;

	if (AudioOutputRegistrar::qmNew)
		keys = AudioOutputRegistrar::qmNew->keys();
	else
		keys.clear();
	i = keys.indexOf(AudioOutputRegistrar::current);
	if (i >= 0)
		loadComboBox(qcbSystem, i);

	loadCheckBox(qcbExclusive, r.bExclusiveOutput);
	loadSlider(qsDelay, r.iOutputDelay);
	loadSlider(qsVolume, static_cast< int >(r.fVolume * 100.0f + 0.5f));
	loadSlider(qsOtherVolume, static_cast< int >((1.0f - r.fOtherVolume) * 100.0f + 0.5f));
	loadCheckBox(qcbAttenuateOthersOnTalk, r.bAttenuateOthersOnTalk);
	loadCheckBox(qcbAttenuateOthers, r.bAttenuateOthers);
	loadCheckBox(qcbAttenuateUsersOnPrioritySpeak, r.bAttenuateUsersOnPrioritySpeak);
	loadCheckBox(qcbOnlyAttenuateSameOutput, r.bOnlyAttenuateSameOutput);
	loadCheckBox(qcbAttenuateLoopbacks, r.bAttenuateLoopbacks);

	const bool attenuationActive = r.bAttenuateOthers || r.bAttenuateOthersOnTalk;
	qcbOnlyAttenuateSameOutput->setEnabled(attenuationActive);
	qcbAttenuateLoopbacks->setEnabled(attenuationActive && r.bOnlyAttenuateSameOutput);

	enablePulseAudioAttenuationOptionsFor(AudioOutputRegistrar::current);

	loadSlider(qsJitter, r.iJitterBufferSize);
	loadComboBox(qcbLoopback, r.lmLoopMode);
	loadSlider(qsPacketDelay, static_cast< int >(r.dMaxPacketDelay));
	loadSlider(qsPacketLoss, static_cast< int >(r.dPacketLoss * 100.0f + 0.5f));
	qsbMinimumDistance->setValue(r.fAudioMinDistance);
	qsbMaximumDistance->setValue(r.fAudioMaxDistance);
	qsbMinimumVolume->setValue(static_cast< int >(r.fAudioMaxDistVolume * 100));
	qsbBloom->setValue(static_cast< int >(r.fAudioBloom * 100));
	loadCheckBox(qcbHeadphones, r.bPositionalHeadphone);
	loadCheckBox(qcbPositional, r.bPositionalAudio);

	qsOtherVolume->setEnabled(r.bAttenuateOthersOnTalk || r.bAttenuateOthers);
	qlOtherVolume->setEnabled(r.bAttenuateOthersOnTalk || r.bAttenuateOthers);
	qcbAttenuateLoopbacks->setEnabled(r.bOnlyAttenuateSameOutput);
}

void AudioOutputDialog::save() const {
	s.iOutputDelay                   = qsDelay->value();
	s.fVolume                        = static_cast< float >(qsVolume->value()) / 100.0f;
	s.fOtherVolume                   = 1.0f - (static_cast< float >(qsOtherVolume->value()) / 100.0f);
	s.bAttenuateOthersOnTalk         = qcbAttenuateOthersOnTalk->isChecked();
	s.bAttenuateOthers               = qcbAttenuateOthers->isChecked();
	s.bOnlyAttenuateSameOutput       = qcbOnlyAttenuateSameOutput->isChecked();
	s.bAttenuateLoopbacks            = qcbAttenuateLoopbacks->isChecked();
	s.bAttenuateUsersOnPrioritySpeak = qcbAttenuateUsersOnPrioritySpeak->isChecked();
	s.iJitterBufferSize              = qsJitter->value();
	s.qsAudioOutput                  = qcbSystem->currentText();
	s.lmLoopMode                     = static_cast< Settings::LoopMode >(qcbLoopback->currentIndex());
	s.dMaxPacketDelay                = static_cast< float >(qsPacketDelay->value());
	s.dPacketLoss                    = static_cast< float >(qsPacketLoss->value()) / 100.0f;
	s.fAudioMinDistance              = static_cast< float >(qsbMinimumDistance->value());
	s.fAudioMaxDistance              = static_cast< float >(qsbMaximumDistance->value());
	s.fAudioMaxDistVolume            = static_cast< float >(qsbMinimumVolume->value()) / 100.0f;
	s.fAudioBloom                    = static_cast< float >(qsbBloom->value()) / 100.0f;
	s.bPositionalAudio               = qcbPositional->isChecked();
	s.bPositionalHeadphone           = qcbHeadphones->isChecked();
	s.bExclusiveOutput               = qcbExclusive->isChecked();


	if (AudioOutputRegistrar::qmNew) {
		AudioOutputRegistrar *aor = AudioOutputRegistrar::qmNew->value(qcbSystem->currentText());
		int idx                   = qcbDevice->currentIndex();
		if (idx > -1) {
			aor->setDeviceChoice(qcbDevice->itemData(idx), s);
		}
	}
}

void AudioOutputDialog::on_qcbSystem_currentIndexChanged(int) {
	qcbDevice->clear();

	QList< audioDevice > choices;

	if (AudioOutputRegistrar::qmNew) {
		auto aor         = AudioOutputRegistrar::qmNew->value(qcbSystem->currentText());
		QVariant current = aor->getDeviceChoice();
		choices          = aor->getDeviceChoices();

		for (int i = 0; i < choices.size(); ++i) {
			auto &choice = choices.at(i);
			qcbDevice->addItem(choice.first, choice.second);
			qcbDevice->setItemData(i, choice.first.toHtmlEscaped(), Qt::ToolTipRole);

			if (choice.second == current) {
				qcbDevice->setCurrentIndex(i);
			}
		}

		bool canmute = aor->canMuteOthers();
		qsOtherVolume->setEnabled(canmute);
		qcbAttenuateOthersOnTalk->setEnabled(canmute);

		enablePulseAudioAttenuationOptionsFor(aor->name);

		qcbAttenuateOthers->setEnabled(canmute);
		qlOtherVolume->setEnabled(canmute);

		bool usesdelay = aor->usesOutputDelay();
		qsDelay->setEnabled(usesdelay);
		qlDelay->setEnabled(usesdelay);

		qcbExclusive->setEnabled(aor->canExclusive());
	}

	qcbDevice->setEnabled(!choices.isEmpty());
}

void AudioOutputDialog::on_qsJitter_valueChanged(int v) {
	qlJitter->setText(tr("%1 ms").arg(v * 10));
	Mumble::Accessibility::setSliderSemanticValue(qsJitter, QString("%1 %2").arg(v * 10).arg(tr("milliseconds")));
}

void AudioOutputDialog::on_qsVolume_valueChanged(int v) {
	QPalette pal;

	if (v > 100) {
		pal.setColor(qlVolume->foregroundRole(), Qt::red);
	}
	qlVolume->setPalette(pal);

	qlVolume->setText(tr("%1 %").arg(v));
	Mumble::Accessibility::setSliderSemanticValue(qsVolume, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioOutputDialog::on_qsOtherVolume_valueChanged(int v) {
	qlOtherVolume->setText(tr("%1 %").arg(v));
	Mumble::Accessibility::setSliderSemanticValue(qsOtherVolume, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioOutputDialog::on_qsPacketDelay_valueChanged(int v) {
	qlPacketDelay->setText(tr("%1 ms").arg(v));
	Mumble::Accessibility::setSliderSemanticValue(qsPacketDelay, QString("%1 %2").arg(v).arg(tr("milliseconds")));
}

void AudioOutputDialog::on_qsPacketLoss_valueChanged(int v) {
	qlPacketLoss->setText(tr("%1 %").arg(v));
	Mumble::Accessibility::setSliderSemanticValue(qsPacketLoss, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioOutputDialog::on_qsDelay_valueChanged(int v) {
	qlDelay->setText(tr("%1 ms").arg(v * 10));
	Mumble::Accessibility::setSliderSemanticValue(qsDelay, QString("%1 %2").arg(v * 10).arg(tr("milliseconds")));
}

void AudioOutputDialog::on_qcbLoopback_currentIndexChanged(int v) {
	bool ena = false;
	if (v == 1)
		ena = true;

	qsPacketDelay->setEnabled(ena);
	qlPacketDelay->setEnabled(ena);
	qsPacketLoss->setEnabled(ena);
	qlPacketLoss->setEnabled(ena);
}

void AudioOutputDialog::on_qsMinDistance_valueChanged(int value) {
	QSignalBlocker blocker(qsbMinimumDistance);
	qsbMinimumDistance->setValue(value / 10.0);

	// Ensure that max distance is always a least 1m larger than min distance
	qsbMaximumDistance->setValue(std::max(qsbMaximumDistance->value(), (value / 10.0) + 1));
}

void AudioOutputDialog::on_qsbMinimumDistance_valueChanged(double value) {
	QSignalBlocker blocker(qsMinDistance);
	qsMinDistance->setValue(static_cast< int >(value * 10));
	Mumble::Accessibility::setSliderSemanticValue(qsMinDistance, QString("%1 %2").arg(value * 10).arg(tr("meters")));

	// Ensure that max distance is always a least 1m larger than min distance
	qsMaxDistance->setValue(std::max(qsMaxDistance->value(), static_cast< int >(value * 10) + 10));
}

void AudioOutputDialog::on_qsMaxDistance_valueChanged(int value) {
	QSignalBlocker blocker(qsbMaximumDistance);
	qsbMaximumDistance->setValue(value / 10.0);

	// Ensure that min distance is always a least 1m less than max distance
	qsbMinimumDistance->setValue(std::min(qsbMinimumDistance->value(), (value / 10.0) - 1));
}
void AudioOutputDialog::on_qsbMaximumDistance_valueChanged(double value) {
	QSignalBlocker blocker(qsMaxDistance);
	qsMaxDistance->setValue(static_cast< int >(value * 10));
	Mumble::Accessibility::setSliderSemanticValue(qsMaxDistance, QString("%1 %2").arg(value * 10).arg(tr("meters")));

	// Ensure that min distance is always a least 1m less than max distance
	qsMinDistance->setValue(std::min(qsMinDistance->value(), static_cast< int >(value * 10) - 10));
}

void AudioOutputDialog::on_qsMinimumVolume_valueChanged(int value) {
	QSignalBlocker blocker(qsbMinimumVolume);
	qsbMinimumVolume->setValue(value);
	Mumble::Accessibility::setSliderSemanticValue(qsMinimumVolume, Mumble::Accessibility::SliderMode::READ_PERCENT,
												  "%");
}

void AudioOutputDialog::on_qsbMinimumVolume_valueChanged(int value) {
	QSignalBlocker blocker(qsMinimumVolume);
	qsMinimumVolume->setValue(value);
}

void AudioOutputDialog::on_qsBloom_valueChanged(int value) {
	QSignalBlocker blocker(qsbBloom);
	qsbBloom->setValue(value);
	Mumble::Accessibility::setSliderSemanticValue(qsBloom, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
}

void AudioOutputDialog::on_qsbBloom_valueChanged(int value) {
	QSignalBlocker blocker(qsBloom);
	qsBloom->setValue(value);
}

void AudioOutputDialog::on_qcbAttenuateOthersOnTalk_clicked(bool checked) {
	bool b = qcbAttenuateOthers->isChecked() || checked;
	qsOtherVolume->setEnabled(b);
	qlOtherVolume->setEnabled(b);

	qcbOnlyAttenuateSameOutput->setEnabled(b);
	qcbAttenuateLoopbacks->setEnabled(b && qcbOnlyAttenuateSameOutput->isChecked());
}

void AudioOutputDialog::on_qcbAttenuateOthers_clicked(bool checked) {
	bool b = qcbAttenuateOthersOnTalk->isChecked() || checked;
	qsOtherVolume->setEnabled(b);
	qlOtherVolume->setEnabled(b);

	qcbOnlyAttenuateSameOutput->setEnabled(b);
	qcbAttenuateLoopbacks->setEnabled(b && qcbOnlyAttenuateSameOutput->isChecked());
}

void AudioOutputDialog::on_qcbOnlyAttenuateSameOutput_clicked(bool checked) {
	qcbAttenuateLoopbacks->setEnabled(checked);
}
