// 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 "ConnectDialog.h"

#ifdef USE_ZEROCONF
#	include "Zeroconf.h"
#endif

#include "Channel.h"
#include "Database.h"
#include "ServerHandler.h"
#include "ServerResolver.h"
#include "Utils.h"
#include "WebFetch.h"
#include "Global.h"

#include <QSettings>
#include <QtCore/QMimeData>
#include <QtCore/QUrlQuery>
#include <QtCore/QtEndian>
#include <QtGui/QClipboard>
#include <QtGui/QDesktopServices>
#include <QtGui/QPainter>
#include <QtNetwork/QUdpSocket>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QShortcut>
#include <QtXml/QDomDocument>

#include <boost/accumulators/statistics/extended_p_square.hpp>
#include <boost/array.hpp>

#ifdef Q_OS_WIN
#	ifndef NOMINMAX
#		define NOMINMAX
#	endif
#	include <shlobj.h>
#endif

#include <QRandomGenerator>

#include <algorithm>

QMap< QString, QIcon > ServerItem::qmIcons;
QList< PublicInfo > ConnectDialog::qlPublicServers;
QString ConnectDialog::qsUserCountry, ConnectDialog::qsUserCountryCode, ConnectDialog::qsUserContinentCode;
Timer ConnectDialog::tPublicServers;


PingStats::PingStats() {
	init();
}

PingStats::~PingStats() {
	delete asQuantile;
}

void PingStats::init() {
	boost::array< double, 3 > probs = { { 0.75, 0.80, 0.95 } };

	asQuantile  = new asQuantileType(boost::accumulators::tag::extended_p_square::probabilities = probs);
	dPing       = 0.0;
	uiPing      = 0;
	uiPingSort  = 0;
	uiUsers     = 0;
	uiMaxUsers  = 0;
	uiBandwidth = 0;
	uiSent      = 0;
	uiRecv      = 0;
	m_version   = Version::UNKNOWN;
}

void PingStats::reset() {
	delete asQuantile;
	init();
}

ServerViewDelegate::ServerViewDelegate(QObject *p) : QStyledItemDelegate(p) {
}

ServerViewDelegate::~ServerViewDelegate() {
}

void ServerViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
	// Allow a ServerItem's BackgroundRole to override the current theme's default color.
	QVariant bg = index.data(Qt::BackgroundRole);
	if (bg.isValid()) {
		painter->fillRect(option.rect, bg.value< QBrush >());
	}

	QStyledItemDelegate::paint(painter, option, index);
}

ServerView::ServerView(QWidget *p) : QTreeWidget(p) {
	siFavorite = new ServerItem(tr("Favorite"), ServerItem::FavoriteType);
	addTopLevelItem(siFavorite);
	siFavorite->setExpanded(true);
	siFavorite->setHidden(true);

#ifdef USE_ZEROCONF
	siLAN = new ServerItem(tr("LAN"), ServerItem::LANType);
	addTopLevelItem(siLAN);
	siLAN->setExpanded(true);
	siLAN->setHidden(true);
#else
	siLAN         = nullptr;
#endif

	if (!Global::get().s.bDisablePublicList) {
		siPublic = new ServerItem(tr("Public Internet"), ServerItem::PublicType);
		siPublic->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
		addTopLevelItem(siPublic);

		siPublic->setExpanded(false);
	} else {
		qWarning() << "Public list disabled";

		siPublic = nullptr;
	}
}

ServerView::~ServerView() {
	delete siFavorite;
	delete siLAN;
	delete siPublic;
}

QMimeData *ServerView::mimeData(const QList< QTreeWidgetItem * > mimeitems) const {
	if (mimeitems.isEmpty())
		return nullptr;

	ServerItem *si = static_cast< ServerItem * >(mimeitems.first());
	return si->toMimeData();
}

QStringList ServerView::mimeTypes() const {
	QStringList qsl;
	qsl << QStringList(QLatin1String("text/uri-list"));
	qsl << QStringList(QLatin1String("text/plain"));
	return qsl;
}

Qt::DropActions ServerView::supportedDropActions() const {
	return Qt::CopyAction | Qt::LinkAction;
}

/* Extract and append (2), (3) etc to the end of a servers name if it is cloned. */
void ServerView::fixupName(ServerItem *si) {
	QString name = si->qsName;

	int tag = 1;

	QRegExp tmatch(QLatin1String("(.+)\\((\\d+)\\)"));
	tmatch.setMinimal(true);
	if (tmatch.exactMatch(name)) {
		name = tmatch.capturedTexts().at(1).trimmed();
		tag  = tmatch.capturedTexts().at(2).toInt();
	}

	bool found;
	QString cmpname;
	do {
		found = false;
		if (tag > 1)
			cmpname = name + QString::fromLatin1(" (%1)").arg(tag);
		else
			cmpname = name;

		foreach (ServerItem *f, siFavorite->qlChildren)
			if (f->qsName == cmpname)
				found = true;

		++tag;
	} while (found);

	si->qsName = cmpname;
}

bool ServerView::dropMimeData(QTreeWidgetItem *, int, const QMimeData *mime, Qt::DropAction) {
	ServerItem *si = ServerItem::fromMimeData(mime);
	if (!si)
		return false;

	fixupName(si);

	qobject_cast< ConnectDialog * >(parent())->qlItems << si;
	siFavorite->addServerItem(si);

	qobject_cast< ConnectDialog * >(parent())->startDns(si);

	setCurrentItem(si);

	return true;
}

void ServerItem::init() {
	// Without this, columncount is wrong.
	setData(0, Qt::DisplayRole, QVariant());
	setData(1, Qt::DisplayRole, QVariant());
	setData(2, Qt::DisplayRole, QVariant());
	emitDataChanged();
}

ServerItem::ServerItem(const FavoriteServer &fs) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
	siParent = nullptr;
	bParent  = false;

	itType = FavoriteType;
	qsName = fs.qsName;
	usPort = fs.usPort;

	qsUsername = fs.qsUsername;
	qsPassword = fs.qsPassword;

	qsUrl = fs.qsUrl;

	bCA = false;
#ifdef USE_ZEROCONF
	if (fs.qsHostname.startsWith(QLatin1Char('@'))) {
		zeroconfHost   = fs.qsHostname.mid(1);
		zeroconfRecord = BonjourRecord(zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
	} else {
		qsHostname = fs.qsHostname;
	}
#else
	qsHostname    = fs.qsHostname;
#endif
	init();
}

ServerItem::ServerItem(const PublicInfo &pi) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
	siParent        = nullptr;
	bParent         = false;
	itType          = PublicType;
	qsName          = pi.qsName;
	qsHostname      = pi.qsIp;
	usPort          = pi.usPort;
	qsUrl           = pi.quUrl.toString();
	qsCountry       = pi.qsCountry;
	qsCountryCode   = pi.qsCountryCode;
	qsContinentCode = pi.qsContinentCode;
	bCA             = pi.bCA;

	init();
}

ServerItem::ServerItem(const QString &name, const QString &host, unsigned short port, const QString &username,
					   const QString &password)
	: QTreeWidgetItem(QTreeWidgetItem::UserType) {
	siParent   = nullptr;
	bParent    = false;
	itType     = FavoriteType;
	qsName     = name;
	usPort     = port;
	qsUsername = username;
	qsPassword = password;

	bCA = false;
#ifdef USE_ZEROCONF
	if (host.startsWith(QLatin1Char('@'))) {
		zeroconfHost   = host.mid(1);
		zeroconfRecord = BonjourRecord(zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
	} else {
		qsHostname = host;
	}
#else
	qsHostname    = host;
#endif
	init();
}

#ifdef USE_ZEROCONF
ServerItem::ServerItem(const BonjourRecord &br) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
	siParent       = nullptr;
	bParent        = false;
	itType         = LANType;
	qsName         = br.serviceName;
	zeroconfHost   = qsName;
	zeroconfRecord = br;
	usPort         = 0;
	bCA            = false;

	init();
}
#endif

ServerItem::ServerItem(const QString &name, ItemType itype) {
	siParent = nullptr;
	bParent  = true;
	qsName   = name;
	itType   = itype;
	setFlags(flags() & ~Qt::ItemIsDragEnabled);
	bCA = false;

	init();
}

ServerItem::ServerItem(const ServerItem *si) {
	siParent = nullptr;
	bParent  = false;
	itType   = FavoriteType;

	qsName          = si->qsName;
	qsHostname      = si->qsHostname;
	usPort          = si->usPort;
	qsUsername      = si->qsUsername;
	qsPassword      = si->qsPassword;
	qsCountry       = si->qsCountry;
	qsCountryCode   = si->qsCountryCode;
	qsContinentCode = si->qsContinentCode;
	qsUrl           = si->qsUrl;
#ifdef USE_ZEROCONF
	zeroconfHost   = si->zeroconfHost;
	zeroconfRecord = si->zeroconfRecord;
#endif
	qlAddresses = si->qlAddresses;
	bCA         = si->bCA;

	m_version   = si->m_version;
	uiPing      = si->uiPing;
	uiPingSort  = si->uiPing;
	uiUsers     = si->uiUsers;
	uiMaxUsers  = si->uiMaxUsers;
	uiBandwidth = si->uiBandwidth;
	uiSent      = si->uiSent;
	dPing       = si->dPing;
	*asQuantile = *si->asQuantile;
}

ServerItem::~ServerItem() {
	if (siParent) {
		siParent->qlChildren.removeAll(this);
		if (siParent->bParent && siParent->qlChildren.isEmpty())
			siParent->setHidden(true);
	}

	// This is just for cleanup when exiting the dialog, it won't stop pending DNS for the children.
	foreach (ServerItem *si, qlChildren)
		delete si;
}

ServerItem *ServerItem::fromMimeData(const QMimeData *mime, bool default_name, QWidget *p, bool convertHttpUrls) {
	if (mime->hasFormat(QLatin1String("OriginatedInMumble")))
		return nullptr;

	QUrl url;
	if (mime->hasUrls() && !mime->urls().isEmpty())
		url = mime->urls().at(0);
	else if (mime->hasText())
		url = QUrl::fromEncoded(mime->text().toUtf8());

	QString qsFile = url.toLocalFile();
	if (!qsFile.isEmpty()) {
		QFile f(qsFile);
		// Make sure we don't accidentally read something big the user
		// happened to have in his clipboard. We only want to look
		// at small link files.
		if (f.open(QIODevice::ReadOnly) && f.size() < 10240) {
			QByteArray qba = f.readAll();
			f.close();

			url = QUrl::fromEncoded(qba, QUrl::StrictMode);
			if (!url.isValid()) {
				// Windows internet shortcut files (.url) are an ini with an URL value
				QSettings qs(qsFile, QSettings::IniFormat);
				url =
					QUrl::fromEncoded(qs.value(QLatin1String("InternetShortcut/URL")).toByteArray(), QUrl::StrictMode);
			}
		}
	}

	if (default_name) {
		QUrlQuery query(url);
		if (!query.hasQueryItem(QLatin1String("title"))) {
			query.addQueryItem(QLatin1String("title"), url.host());
		}
	}

	if (!url.isValid()) {
		return nullptr;
	}

	// An URL from text without a scheme will have the hostname text
	// in the QUrl scheme and no hostname. We do not want to use that.
	if (url.host().isEmpty()) {
		return nullptr;
	}

	// Some communication programs automatically create http links from domains.
	// When a user sends another user a domain to connect to, and http is added wrongly,
	// we do our best to remove it again.
	if (convertHttpUrls && (url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("https"))) {
		url.setScheme(QLatin1String("mumble"));
	}

	return fromUrl(url, p);
}

ServerItem *ServerItem::fromUrl(QUrl url, QWidget *p) {
	if (!url.isValid() || (url.scheme() != QLatin1String("mumble"))) {
		return nullptr;
	}

	QUrlQuery query(url);

	if (url.userName().isEmpty()) {
		if (Global::get().s.qsUsername.isEmpty()) {
			bool ok;
			QString defUserName = QInputDialog::getText(p, ConnectDialog::tr("Adding host %1").arg(url.host()),
														ConnectDialog::tr("Enter username"), QLineEdit::Normal,
														Global::get().s.qsUsername, &ok)
									  .trimmed();
			if (!ok)
				return nullptr;
			if (defUserName.isEmpty())
				return nullptr;
			Global::get().s.qsUsername = defUserName;
		}
		url.setUserName(Global::get().s.qsUsername);
	}

	ServerItem *si =
		new ServerItem(query.queryItemValue(QLatin1String("title")), url.host(),
					   static_cast< unsigned short >(url.port(DEFAULT_MUMBLE_PORT)), url.userName(), url.password());

	if (query.hasQueryItem(QLatin1String("url")))
		si->qsUrl = query.queryItemValue(QLatin1String("url"));

	return si;
}

QVariant ServerItem::data(int column, int role) const {
	if (bParent) {
		if (column == 0) {
			switch (role) {
				case Qt::DisplayRole:
					return qsName;
				case Qt::DecorationRole:
					if (itType == FavoriteType)
						return loadIcon(QLatin1String("skin:emblems/emblem-favorite.svg"));
					else if (itType == LANType)
						return loadIcon(QLatin1String("skin:places/network-workgroup.svg"));
					else
						return loadIcon(QLatin1String("skin:categories/applications-internet.svg"));
			}
		}
	} else {
		if (role == Qt::DecorationRole && column == 0) {
			QString flag;
			if (!qsCountryCode.isEmpty()) {
				flag = QString::fromLatin1(":/flags/%1.svg").arg(qsCountryCode);
				if (!QFileInfo(flag).exists()) {
					flag = QLatin1String("skin:categories/applications-internet.svg");
				}
			} else {
				flag = QLatin1String("skin:categories/applications-internet.svg");
			}
			return loadIcon(flag);
		}
		if (role == Qt::DisplayRole) {
			switch (column) {
				case 0:
					return qsName;
				case 1:
					return (dPing > 0.0) ? QString::number(uiPing) : QVariant();
				case 2:
					return uiUsers ? QString::fromLatin1("%1/%2 ").arg(uiUsers).arg(uiMaxUsers) : QVariant();
			}
		} else if (role == Qt::ToolTipRole) {
			QStringList ipv4List;
			QStringList ipv6List;
			foreach (const ServerAddress &addr, qlAddresses) {
				const QString address = addr.host.toString(false).toHtmlEscaped();
				if (addr.host.isV6()) {
					ipv6List << address;
				} else {
					ipv4List << address;
				}
			}
			QString ipv4 = "-";
			QString ipv6 = "-";
			if (!ipv4List.isEmpty()) {
				ipv4 = ipv4List.join(QLatin1String(", "));
			}
			if (!ipv6List.isEmpty()) {
				ipv6 = ipv6List.join(QLatin1String(", "));
			}

			double ploss = 100.0;

			if (uiSent > 0)
				ploss = (uiSent - std::min(uiRecv, uiSent)) * 100. / uiSent;

			QString qs;
			qs += QLatin1String("<table>")
				  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						.arg(ConnectDialog::tr("Servername"), qsName.toHtmlEscaped())
				  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						.arg(ConnectDialog::tr("Hostname"), qsHostname.toHtmlEscaped());
#ifdef USE_ZEROCONF
			if (!zeroconfHost.isEmpty())
				qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						  .arg(ConnectDialog::tr("Bonjour name"), zeroconfHost.toHtmlEscaped());
#endif
			qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
					  .arg(ConnectDialog::tr("Port"))
					  .arg(usPort)
				  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						.arg(ConnectDialog::tr("IPv4 address"), ipv4)
				  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						.arg(ConnectDialog::tr("IPv6 address"), ipv6);

			if (!qsUrl.isEmpty())
				qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						  .arg(ConnectDialog::tr("Website"), qsUrl.toHtmlEscaped());

			if (uiSent > 0) {
				qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
						  .arg(ConnectDialog::tr("Packet loss"), QString::fromLatin1("%1% (%2/%3)")
																	 .arg(ploss, 0, 'f', 1)
																	 .arg(uiSent - std::min(uiRecv, uiSent))
																	 .arg(uiSent));
				if (uiRecv > 0) {
					qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
							  .arg(ConnectDialog::tr("Ping (80%)"),
								   ConnectDialog::tr("%1 ms").arg(
									   boost::accumulators::extended_p_square(*asQuantile)[1] / 1000., 0, 'f', 2))
						  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
								.arg(ConnectDialog::tr("Ping (95%)"),
									 ConnectDialog::tr("%1 ms").arg(
										 boost::accumulators::extended_p_square(*asQuantile)[2] / 1000., 0, 'f', 2))
						  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
								.arg(ConnectDialog::tr("Bandwidth"),
									 ConnectDialog::tr("%1 kbit/s").arg(uiBandwidth / 1000))
						  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
								.arg(ConnectDialog::tr("Users"),
									 QString::fromLatin1("%1/%2").arg(uiUsers).arg(uiMaxUsers))
						  + QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>")
								.arg(ConnectDialog::tr("Version"))
								.arg(Version::toString(m_version));
				}
			}
			qs += QLatin1String("</table>");
			return qs;
		} else if (role == Qt::BackgroundRole) {
			if (bCA) {
				QColor qc(Qt::green);
				qc.setAlpha(32);
				return qc;
			}
		} else if (role == Qt::AccessibleTextRole) {
			return QString("%1 %2").arg(ConnectDialog::tr("Server")).arg(qsName);
		}
	}
	return QTreeWidgetItem::data(column, role);
}

void ServerItem::addServerItem(ServerItem *childitem) {
	Q_ASSERT(!childitem->siParent);

	childitem->siParent = this;
	qlChildren.append(childitem);
	addChild(childitem);
	// Public servers must initially be hidden for the search to work properly
	// They will be set to visible later on
	if (childitem->itType == PublicType) {
		childitem->setHidden(true);
	}

	if (bParent && (itType != PublicType) && isHidden()) {
		setHidden(false);
	}
}

void ServerItem::setDatas(double elapsed, quint32 users, quint32 maxusers) {
	if (elapsed == 0.0) {
		emitDataChanged();
		return;
	}

	(*asQuantile)(static_cast< double >(elapsed));
	dPing = boost::accumulators::extended_p_square(*asQuantile)[0];
	if (dPing == 0.0)
		dPing = elapsed;

	quint32 ping = static_cast< quint32 >(lround(dPing / 1000.));
	uiRecv       = static_cast< quint32 >(boost::accumulators::count(*asQuantile));

	bool changed = (ping != uiPing) || (users != uiUsers) || (maxusers != uiMaxUsers);

	uiUsers    = users;
	uiMaxUsers = maxusers;
	uiPing     = ping;

	double grace = qMax(5000., 50. * uiPingSort);
	double diff  = fabs(1000. * uiPingSort - dPing);

	if ((uiPingSort == 0) || ((uiSent >= 10) && (diff >= grace)))
		uiPingSort = ping;

	if (changed)
		emitDataChanged();
}

FavoriteServer ServerItem::toFavoriteServer() const {
	FavoriteServer fs;
	fs.qsName = qsName;
#ifdef USE_ZEROCONF
	if (!zeroconfHost.isEmpty())
		fs.qsHostname = QLatin1Char('@') + zeroconfHost;
	else
		fs.qsHostname = qsHostname;
#else
	fs.qsHostname = qsHostname;
#endif
	fs.usPort     = usPort;
	fs.qsUsername = qsUsername;
	fs.qsPassword = qsPassword;
	fs.qsUrl      = qsUrl;
	return fs;
}

/**
 * This function turns a ServerItem object into a QMimeData object holding a URL to the server.
 */
QMimeData *ServerItem::toMimeData() const {
	QMimeData *mime = ServerItem::toMimeData(qsName, qsHostname, usPort);

	if (itType == FavoriteType)
		mime->setData(QLatin1String("OriginatedInMumble"), QByteArray());

	return mime;
}

/**
 * This function creates a QMimeData object containing a URL to the server at host and port. name is passed in the
 * query string as "title", which is used for adding a server to favorites. channel may be omitted, but if specified it
 * should be in the format of "/path/to/channel".
 */
QMimeData *ServerItem::toMimeData(const QString &name, const QString &host, unsigned short port,
								  const QString &channel) {
	QUrl url;
	url.setScheme(QLatin1String("mumble"));
	url.setHost(host);
	if (port != DEFAULT_MUMBLE_PORT)
		url.setPort(port);
	url.setPath(channel);

	QUrlQuery query;
	query.addQueryItem(QLatin1String("title"), name);
	query.addQueryItem(QLatin1String("version"), QLatin1String("1.2.0"));
	url.setQuery(query);

	QString qs = QLatin1String(url.toEncoded());

	QMimeData *mime = new QMimeData;

#ifdef Q_OS_WIN
	QString contents = QString::fromLatin1("[InternetShortcut]\r\nURL=%1\r\n").arg(qs);
	QString urlname  = QString::fromLatin1("%1.url").arg(name);

	FILEGROUPDESCRIPTORA fgda;
	ZeroMemory(&fgda, sizeof(fgda));
	fgda.cItems              = 1;
	fgda.fgd[0].dwFlags      = FD_LINKUI | FD_FILESIZE;
	fgda.fgd[0].nFileSizeLow = contents.length();
	strcpy_s(fgda.fgd[0].cFileName, MAX_PATH, urlname.toLocal8Bit().constData());
	mime->setData(QLatin1String("FileGroupDescriptor"),
				  QByteArray(reinterpret_cast< const char * >(&fgda), sizeof(fgda)));

	FILEGROUPDESCRIPTORW fgdw;
	ZeroMemory(&fgdw, sizeof(fgdw));
	fgdw.cItems              = 1;
	fgdw.fgd[0].dwFlags      = FD_LINKUI | FD_FILESIZE;
	fgdw.fgd[0].nFileSizeLow = contents.length();
	wcscpy_s(fgdw.fgd[0].cFileName, MAX_PATH, urlname.toStdWString().c_str());
	mime->setData(QLatin1String("FileGroupDescriptorW"),
				  QByteArray(reinterpret_cast< const char * >(&fgdw), sizeof(fgdw)));

	mime->setData(QString::fromWCharArray(CFSTR_FILECONTENTS), contents.toLocal8Bit());

	DWORD context[4];
	context[0] = 0;
	context[1] = 1;
	context[2] = 0;
	context[3] = 0;
	mime->setData(QLatin1String("DragContext"),
				  QByteArray(reinterpret_cast< const char * >(&context[0]), sizeof(context)));

	DWORD dropaction;
	dropaction = DROPEFFECT_LINK;
	mime->setData(QString::fromWCharArray(CFSTR_PREFERREDDROPEFFECT),
				  QByteArray(reinterpret_cast< const char * >(&dropaction), sizeof(dropaction)));
#endif
	QList< QUrl > urls;
	urls << url;
	mime->setUrls(urls);

	mime->setText(qs);
	mime->setHtml(QString::fromLatin1("<a href=\"%1\">%2</a>").arg(qs).arg(name.toHtmlEscaped()));

	return mime;
}

bool ServerItem::operator<(const QTreeWidgetItem &o) const {
	const ServerItem &other = static_cast< const ServerItem & >(o);
	const QTreeWidget *w    = treeWidget();

	const int column = w ? w->sortColumn() : 0;

	if (itType != other.itType) {
		const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
		bool less;

		if (itType == FavoriteType)
			less = true;
		else if ((itType == LANType) && (other.itType == PublicType))
			less = true;
		else
			less = false;
		return less ^ inverse;
	}

	if (bParent) {
		const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
		return (qsName < other.qsName) ^ inverse;
	}

	if (column == 0) {
		QString a = qsName.toLower();
		QString b = other.qsName.toLower();

		QRegExp re(QLatin1String("[^0-9a-z]"));
		a.remove(re);
		b.remove(re);
		return a < b;
	} else if (column == 1) {
		quint32 a = uiPingSort ? uiPingSort : UINT_MAX;
		quint32 b = other.uiPingSort ? other.uiPingSort : UINT_MAX;
		return a < b;
	} else if (column == 2) {
		return uiUsers < other.uiUsers;
	}
	return false;
}

QIcon ServerItem::loadIcon(const QString &name) {
	if (!qmIcons.contains(name))
		qmIcons.insert(name, QIcon(name));
	return qmIcons.value(name);
}

ConnectDialogEdit::ConnectDialogEdit(QWidget *p, const QString &name, const QString &host, const QString &user,
									 unsigned short port, const QString &password)
	: QDialog(p) {
	setupUi(this);
	init();

	bCustomLabel = !name.simplified().isEmpty();

	qleName->setText(name);
	qleServer->setText(host);
	qleUsername->setText(user);
	qlePort->setText(QString::number(port));
	qlePassword->setText(password);

	validate();
}

ConnectDialogEdit::ConnectDialogEdit(QWidget *parent) : QDialog(parent) {
	setupUi(this);
	setWindowTitle(tr("Add Server"));
	init();

	if (!updateFromClipboard()) {
		// If connected to a server assume the user wants to add it
		if (Global::get().sh && Global::get().sh->isRunning()) {
			QString host, name, user, pw;
			unsigned short port = DEFAULT_MUMBLE_PORT;

			Global::get().sh->getConnectionInfo(host, port, user, pw);
			Channel *c = Channel::get(Channel::ROOT_ID);
			if (c && c->qsName != QLatin1String("Root")) {
				name = c->qsName;
			}

			showNotice(tr("You are currently connected to a server.\nDo you want to fill the dialog with the "
						  "connection data of this server?\nHost: %1 Port: %2")
						   .arg(host)
						   .arg(port));
			m_si = new ServerItem(name, host, port, user, pw);
		}
	}
	qleUsername->setText(Global::get().s.qsUsername);
}

void ConnectDialogEdit::init() {
	m_si         = nullptr;
	usPort       = 0;
	bOk          = true;
	bCustomLabel = false;

	qwInlineNotice->hide();

	qlePort->setValidator(new QIntValidator(1, 65535, qlePort));
	qlePort->setText(QString::number(DEFAULT_MUMBLE_PORT));
	qlePassword->setEchoMode(QLineEdit::Password);

	connect(qleName, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
	connect(qleServer, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
	connect(qlePort, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
	connect(qleUsername, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
	connect(qlePassword, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));

	validate();
}

ConnectDialogEdit::~ConnectDialogEdit() {
	delete m_si;
}

void ConnectDialogEdit::showNotice(const QString &text) {
	QLabel *label = qwInlineNotice->findChild< QLabel * >(QLatin1String("qlPasteNotice"));
	Q_ASSERT(label);
	label->setText(text);
	qwInlineNotice->show();
	adjustSize();
}

bool ConnectDialogEdit::updateFromClipboard() {
	delete m_si;
	m_si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData(), false, nullptr, true);
	if (m_si) {
		showNotice(
			tr("You have an URL in your clipboard.\nDo you want to fill the dialog with this data?\nHost: %1 Port: %2")
				.arg(m_si->qsHostname)
				.arg(m_si->usPort));
		return true;
	} else {
		qwInlineNotice->hide();
		adjustSize();
		return false;
	}
}

void ConnectDialogEdit::on_qbFill_clicked() {
	Q_ASSERT(m_si);

	qwInlineNotice->hide();
	adjustSize();

	qleName->setText(m_si->qsName);
	qleServer->setText(m_si->qsHostname);
	qleUsername->setText(m_si->qsUsername);
	qlePort->setText(QString::number(m_si->usPort));
	qlePassword->setText(m_si->qsPassword);

	delete m_si;
	m_si = nullptr;
}

void ConnectDialogEdit::on_qbDiscard_clicked() {
	qwInlineNotice->hide();
	adjustSize();
}

void ConnectDialogEdit::on_qleName_textEdited(const QString &name) {
	if (bCustomLabel) {
		// If empty, then reset to automatic label.
		// NOTE(nik@jnstw.us): You may be tempted to set qleName to qleServer, but that results in the odd
		// UI behavior that clearing the field doesn't clear it; it'll immediately equal qleServer. Instead,
		// leave it empty and let it update the next time qleServer updates. Code in accept will default it
		// to qleServer if it isn't updated beforehand.
		if (name.simplified().isEmpty()) {
			bCustomLabel = false;
		}
	} else {
		// If manually edited, set to Custom
		bCustomLabel = true;
	}
}

void ConnectDialogEdit::on_qleServer_textEdited(const QString &server) {
	// If using automatic label, update it
	if (!bCustomLabel) {
		qleName->setText(server);
	}
}

void ConnectDialogEdit::validate() {
	qsName     = qleName->text().simplified();
	qsHostname = qleServer->text().simplified();
	usPort     = qlePort->text().toUShort();
	qsUsername = qleUsername->text().simplified();
	qsPassword = qlePassword->text();

	// For bonjour hosts disable the port field as it's auto-detected
	qlePort->setDisabled(!qsHostname.isEmpty() && qsHostname.startsWith(QLatin1Char('@')));

	// For SuperUser show password edit
	if (qsUsername.toLower() == QLatin1String("superuser")) {
		qliPassword->setVisible(true);
		qlePassword->setVisible(true);
		qcbShowPassword->setVisible(true);
		adjustSize();
	} else if (qsPassword.isEmpty()) {
		qliPassword->setVisible(false);
		qlePassword->setVisible(false);
		qcbShowPassword->setVisible(false);
		adjustSize();
	}

	bOk = !qsHostname.isEmpty() && !qsUsername.isEmpty() && usPort;
	qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);
}

void ConnectDialogEdit::accept() {
	validate();
	if (bOk) {
		QString server = qleServer->text().simplified();

		// If the user accidentally added a schema or path part, drop it now.
		// We can't do so during editing as that is quite jarring.
		const int schemaPos = server.indexOf(QLatin1String("://"));
		if (schemaPos != -1) {
			server.remove(0, schemaPos + 3);
		}

		const int pathPos = server.indexOf(QLatin1Char('/'));
		if (pathPos != -1) {
			server.resize(pathPos);
		}

		qleServer->setText(server);

		if (qleName->text().simplified().isEmpty() || !bCustomLabel) {
			qleName->setText(server);
		}

		QDialog::accept();
	}
}

void ConnectDialogEdit::on_qcbShowPassword_toggled(bool checked) {
	qlePassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
}

ConnectDialog::ConnectDialog(QWidget *p, bool autoconnect) : QDialog(p), bAutoConnect(autoconnect) {
	setupUi(this);
#ifdef Q_OS_MAC
	setWindowModality(Qt::WindowModal);
#endif
	bPublicInit = false;

	siAutoConnect = nullptr;

	bAllowPing       = Global::get().s.ptProxyType == Settings::NoProxy;
	bAllowHostLookup = Global::get().s.ptProxyType == Settings::NoProxy;
	bAllowZeroconf   = Global::get().s.ptProxyType == Settings::NoProxy;
	bAllowFilters    = Global::get().s.ptProxyType == Settings::NoProxy;

	if (tPublicServers.elapsed() >= 60 * 24 * 1000000ULL) {
		qlPublicServers.clear();
	}

	qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
	qdbbButtonBox->button(QDialogButtonBox::Ok)->setText(tr("C&onnect"));
	qdbbButtonBox->button(QDialogButtonBox::Ok)->setFocusPolicy(Qt::TabFocus);

	qdbbButtonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::TabFocus);
	qdbbButtonBox->button(QDialogButtonBox::Cancel)->setAutoDefault(false);

	QPushButton *qpbAdd = new QPushButton(tr("&Add New..."), this);
	qpbAdd->setDefault(false);
	qpbAdd->setAutoDefault(false);
	connect(qpbAdd, SIGNAL(clicked()), qaFavoriteAddNew, SIGNAL(triggered()));
	qdbbButtonBox->addButton(qpbAdd, QDialogButtonBox::ActionRole);


	qpbEdit = new QPushButton(tr("&Edit..."), this);
	qpbEdit->setEnabled(false);
	qpbEdit->setDefault(false);
	qpbEdit->setAutoDefault(false);
	connect(qpbEdit, SIGNAL(clicked()), qaFavoriteEdit, SIGNAL(triggered()));
	qdbbButtonBox->addButton(qpbEdit, QDialogButtonBox::ActionRole);

	qpbAdd->setHidden(Global::get().s.disableConnectDialogEditing);
	qpbEdit->setHidden(Global::get().s.disableConnectDialogEditing);

	qtwServers->setItemDelegate(new ServerViewDelegate());

	if (!Global::get().s.bDisablePublicList) {
		const QIcon qiFlag = ServerItem::loadIcon(QLatin1String("skin:categories/applications-internet.svg"));
		// Add continents and 'Unknown' to the location combobox
		qcbSearchLocation->addItem(qiFlag, tr("All"), QLatin1String("all"));
		qcbSearchLocation->addItem(qiFlag, tr("Africa"), QLatin1String("af"));
		qcbSearchLocation->addItem(qiFlag, tr("Asia"), QLatin1String("as"));
		qcbSearchLocation->addItem(qiFlag, tr("Europe"), QLatin1String("eu"));
		qcbSearchLocation->addItem(qiFlag, tr("North America"), QLatin1String("na"));
		qcbSearchLocation->addItem(qiFlag, tr("Oceania"), QLatin1String("oc"));
		qcbSearchLocation->addItem(qiFlag, tr("South America"), QLatin1String("sa"));
		qcbSearchLocation->addItem(qiFlag, tr("Unknown"), QLatin1String(""));
		addCountriesToSearchLocation();
	}
	qgbSearch->setVisible(false);

	// Hide ping and user count if we are not allowed to ping.
	if (!bAllowPing) {
		qtwServers->setColumnCount(1);
	}

	qtwServers->sortItems(1, Qt::AscendingOrder);

	qtwServers->header()->setSectionResizeMode(0, QHeaderView::Stretch);
	if (qtwServers->columnCount() >= 2) {
		qtwServers->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
	}
	if (qtwServers->columnCount() >= 3) {
		qtwServers->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
	}

	connect(qtwServers->header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), this,
			SLOT(OnSortChanged(int, Qt::SortOrder)));

	if (bAllowFilters) {
		switch (Global::get().s.ssFilter) {
			case Settings::ShowPopulated:
				qcbFilter->setCurrentText(tr("Show Populated"));
				break;
			case Settings::ShowAll:
				qcbFilter->setCurrentText(tr("Show All"));
				break;
			default:
				qcbFilter->setCurrentText(tr("Show Reachable"));
				break;
		}
	} else {
		qcbFilter->setEnabled(false);
	}

	qmPopup = new QMenu(this);

	QList< QTreeWidgetItem * > ql;
	QList< FavoriteServer > favorites = Global::get().db->getFavorites();

	foreach (const FavoriteServer &fs, favorites) {
		ServerItem *si = new ServerItem(fs);
		qlItems << si;
		startDns(si);
		qtwServers->siFavorite->addServerItem(si);
	}
#ifdef USE_ZEROCONF
	if (bAllowZeroconf && Global::get().zeroconf && Global::get().zeroconf->isOk()) {
		connect(Global::get().zeroconf, &Zeroconf::recordsChanged, this, &ConnectDialog::onUpdateLanList);
		connect(Global::get().zeroconf, &Zeroconf::recordResolved, this, &ConnectDialog::onResolved);
		connect(Global::get().zeroconf, &Zeroconf::resolveError, this, &ConnectDialog::onLanResolveError);
		onUpdateLanList(Global::get().zeroconf->currentRecords());

		Global::get().zeroconf->startBrowser(QLatin1String("_mumble._tcp"));
	}
#endif
	qtPingTick = new QTimer(this);
	connect(qtPingTick, SIGNAL(timeout()), this, SLOT(timeTick()));

	qusSocket4 = new QUdpSocket(this);
	qusSocket6 = new QUdpSocket(this);
	bIPv4      = qusSocket4->bind(QHostAddress(QHostAddress::Any), 0);
	bIPv6      = qusSocket6->bind(QHostAddress(QHostAddress::AnyIPv6), 0);
	connect(qusSocket4, SIGNAL(readyRead()), this, SLOT(udpReply()));
	connect(qusSocket6, SIGNAL(readyRead()), this, SLOT(udpReply()));

	if (qtwServers->siFavorite->isHidden() && (!qtwServers->siLAN || qtwServers->siLAN->isHidden())
		&& qtwServers->siPublic) {
		qtwServers->siPublic->setExpanded(true);
	}

	iPingIndex = -1;
	qtPingTick->start(50);

	new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(on_qaFavoriteCopy_triggered()));
	new QShortcut(QKeySequence(QKeySequence::Paste), this, SLOT(on_qaFavoritePaste_triggered()));

	qtwServers->setCurrentItem(nullptr);
	bLastFound = false;

	qmPingCache = Global::get().db->getPingCache();

	if (!Global::get().s.qbaConnectDialogGeometry.isEmpty())
		restoreGeometry(Global::get().s.qbaConnectDialogGeometry);
	if (!Global::get().s.qbaConnectDialogHeader.isEmpty())
		qtwServers->header()->restoreState(Global::get().s.qbaConnectDialogHeader);

	setTabOrder(qtwServers, qleSearchServername);
	setTabOrder(qleSearchServername, qcbSearchLocation);
	setTabOrder(qcbSearchLocation, qcbFilter);
	setTabOrder(qcbFilter, qpbAdd);
	setTabOrder(qpbAdd, qpbEdit);
	setTabOrder(qpbEdit, qdbbButtonBox->button(QDialogButtonBox::Ok));
	setTabOrder(qdbbButtonBox->button(QDialogButtonBox::Ok), qtwServers);
}

ConnectDialog::~ConnectDialog() {
#ifdef USE_ZEROCONF
	if (bAllowZeroconf && Global::get().zeroconf && Global::get().zeroconf->isOk()) {
		Global::get().zeroconf->stopBrowser();
		Global::get().zeroconf->cleanupResolvers();
	}
#endif
	ServerItem::qmIcons.clear();

	QList< FavoriteServer > ql;
	qmPingCache.clear();

	foreach (ServerItem *si, qlItems) {
		if (si->uiPing)
			qmPingCache.insert(UnresolvedServerAddress(si->qsHostname, si->usPort), si->uiPing);

		if (si->itType != ServerItem::FavoriteType)
			continue;
		ql << si->toFavoriteServer();
	}
	Global::get().db->setFavorites(ql);
	Global::get().db->setPingCache(qmPingCache);

	Global::get().s.qbaConnectDialogHeader   = qtwServers->header()->saveState();
	Global::get().s.qbaConnectDialogGeometry = saveGeometry();
}

void ConnectDialog::accept() {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
	if (!si || (bAllowHostLookup && si->qlAddresses.isEmpty()) || si->qsHostname.isEmpty()) {
		qWarning() << "Invalid server";
		return;
	}

	qsPassword = si->qsPassword;
	qsServer   = si->qsHostname;
	usPort     = si->usPort;

	if (si->qsUsername.isEmpty()) {
		bool ok;
		QString defUserName = QInputDialog::getText(this, tr("Connecting to %1").arg(si->qsName), tr("Enter username"),
													QLineEdit::Normal, Global::get().s.qsUsername, &ok)
								  .trimmed();
		if (!ok)
			return;
		Global::get().s.qsUsername = si->qsUsername = defUserName;
	}

	qsUsername = si->qsUsername;

	Global::get().s.qsLastServer = si->qsName;

	QDialog::accept();
}

void ConnectDialog::OnSortChanged(int logicalIndex, Qt::SortOrder) {
	if (logicalIndex != 2) {
		return;
	}

	foreach (ServerItem *si, qlItems) {
		if (si->uiPing && (si->uiPing != si->uiPingSort)) {
			si->uiPingSort = si->uiPing;
			si->setDatas();
		}
	}
}

void ConnectDialog::on_qaFavoriteAdd_triggered() {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
	if (!si || (si->itType == ServerItem::FavoriteType))
		return;

	si = new ServerItem(si);
	qtwServers->fixupName(si);
	qlItems << si;
	qtwServers->siFavorite->addServerItem(si);
	qtwServers->setCurrentItem(si);
	startDns(si);
}

void ConnectDialog::on_qaFavoriteAddNew_triggered() {
	ConnectDialogEdit *cde = new ConnectDialogEdit(this);

	if (cde->exec() == QDialog::Accepted) {
		ServerItem *si = new ServerItem(cde->qsName, cde->qsHostname, cde->usPort, cde->qsUsername, cde->qsPassword);
		qlItems << si;
		qtwServers->siFavorite->addServerItem(si);
		qtwServers->setCurrentItem(si);
		startDns(si);
		qdbbButtonBox->button(QDialogButtonBox::Ok)->setFocus();
	}
	delete cde;
}

void ConnectDialog::on_qaFavoriteEdit_triggered() {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
	if (!si || (si->itType != ServerItem::FavoriteType))
		return;

	QString host;
#ifdef USE_ZEROCONF
	if (!si->zeroconfHost.isEmpty())
		host = QLatin1Char('@') + si->zeroconfHost;
	else
		host = si->qsHostname;
#else
	host          = si->qsHostname;
#endif
	ConnectDialogEdit *cde = new ConnectDialogEdit(this, si->qsName, host, si->qsUsername, si->usPort, si->qsPassword);

	if (cde->exec() == QDialog::Accepted) {
		si->qsName     = cde->qsName;
		si->qsUsername = cde->qsUsername;
		si->qsPassword = cde->qsPassword;
		if ((cde->qsHostname != host) || (cde->usPort != si->usPort)) {
			stopDns(si);

			si->qlAddresses.clear();
			si->reset();

			si->usPort = cde->usPort;
#ifdef USE_ZEROCONF
			if (cde->qsHostname.startsWith(QLatin1Char('@'))) {
				si->qsHostname   = QString();
				si->zeroconfHost = cde->qsHostname.mid(1);
				si->zeroconfRecord =
					BonjourRecord(si->zeroconfHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
			} else {
				si->qsHostname     = cde->qsHostname;
				si->zeroconfHost   = QString();
				si->zeroconfRecord = BonjourRecord();
			}
#else
            si->qsHostname = cde->qsHostname;
#endif
			startDns(si);
		}
		si->setDatas();
	}
	delete cde;
}

void ConnectDialog::on_qaFavoriteRemove_triggered() {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
	if (!si || (si->itType != ServerItem::FavoriteType))
		return;

	stopDns(si);
	qlItems.removeAll(si);
	delete si;
}

void ConnectDialog::on_qaFavoriteCopy_triggered() {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->currentItem());
	if (!si)
		return;

	QApplication::clipboard()->setMimeData(si->toMimeData());
}

void ConnectDialog::on_qaFavoritePaste_triggered() {
	ServerItem *si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData());
	if (!si)
		return;

	qlItems << si;
	qtwServers->siFavorite->addServerItem(si);
	qtwServers->setCurrentItem(si);
	startDns(si);
}

void ConnectDialog::on_qaUrl_triggered() {
	auto *si = static_cast< const ServerItem * >(qtwServers->currentItem());
	if (!si || si->qsUrl.isEmpty()) {
		return;
	}

	const QStringList allowedSchemes = { QLatin1String("http"), QLatin1String("https") };

	const auto url = QUrl(si->qsUrl);
	if (allowedSchemes.contains(url.scheme())) {
		QDesktopServices::openUrl(url);
	} else {
		// Inform user that the requested URL has been blocked
		QMessageBox msgBox;
		msgBox.setText(QObject::tr("<b>Blocked URL scheme \"%1\"</b>").arg(url.scheme()));
		msgBox.setInformativeText(QObject::tr("The URL uses a scheme that has been blocked for security reasons."));
		msgBox.setDetailedText(QObject::tr("Blocked URL: \"%1\"").arg(url.toString()));
		msgBox.setIcon(QMessageBox::Warning);
		msgBox.exec();
	}
}

void ConnectDialog::on_qtwServers_customContextMenuRequested(const QPoint &mpos) {
	ServerItem *si = static_cast< ServerItem * >(qtwServers->itemAt(mpos));
	qmPopup->clear();

	if (si && si->bParent) {
		si = nullptr;
	}

	if (si) {
		if (!Global::get().s.disableConnectDialogEditing) {
			if (si->itType == ServerItem::FavoriteType) {
				qmPopup->addAction(qaFavoriteEdit);
				qmPopup->addAction(qaFavoriteRemove);
			} else {
				qmPopup->addAction(qaFavoriteAdd);
			}
		}

		if (!si->qsUrl.isEmpty()) {
			qmPopup->addAction(qaUrl);
		}
	}

	qmPopup->popup(qtwServers->viewport()->mapToGlobal(mpos), nullptr);
}

void ConnectDialog::on_qtwServers_itemDoubleClicked(QTreeWidgetItem *item, int) {
	qtwServers->setCurrentItem(item);
	accept();
}

void ConnectDialog::on_qtwServers_currentItemChanged(QTreeWidgetItem *item, QTreeWidgetItem *) {
	ServerItem *si = static_cast< ServerItem * >(item);

	if (si->siParent == qtwServers->siFavorite) {
		qpbEdit->setEnabled(true);
	} else {
		qpbEdit->setEnabled(false);
	}

	bool bOk = !si->qlAddresses.isEmpty();
	if (!bAllowHostLookup) {
		bOk = true;
	}
	qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);

	bLastFound = true;
}

void ConnectDialog::on_qtwServers_itemExpanded(QTreeWidgetItem *item) {
	if (qtwServers->siPublic && item == qtwServers->siPublic) {
		if (!Global::get().s.bPingServersDialogViewed) {
			// Ask the user for consent to ping the servers. If the user does
			// not give consent, disable the public server list and return.
			int result = QMessageBox::question(
				this, tr("Consent to the transmission of private data"),
				tr("<p>To measure the latency (ping) of public servers and determine the number of active users, "
				   "your IP address must be transmitted to each public server.</p>"
				   "<p>Do you consent to the transmission of your IP address? If you answer no, the public server "
				   "list will be deactivated. However, you can reactivate it at any time in the network settings.</p>"),
				QMessageBox::Yes | QMessageBox::No);
			Global::get().s.bPingServersDialogViewed = true;
			if (result == QMessageBox::No) {
				Global::get().s.bDisablePublicList = true;
				item->setExpanded(false);
				item->setHidden(true);
				return;
			}
		}
		qgbSearch->setVisible(true);
		initList();
		fillList();
	}

	ServerItem *p = static_cast< ServerItem * >(item);

	foreach (ServerItem *si, p->qlChildren) { startDns(si); }
}

void ConnectDialog::on_qtwServers_itemCollapsed(QTreeWidgetItem *item) {
	if (qtwServers->siPublic && item == qtwServers->siPublic) {
		qgbSearch->setVisible(false);
	}
}

void ConnectDialog::initList() {
	if (bPublicInit || (qlPublicServers.count() > 0))
		return;

	bPublicInit = true;

	QUrl url;
	url.setPath(QLatin1String("/v1/list"));

	QUrlQuery query;
	query.addQueryItem(QLatin1String("version"), Version::getRelease());
	url.setQuery(query);

	WebFetch::fetch(QLatin1String("publist"), url, this, SLOT(fetched(QByteArray, QUrl, QMap< QString, QString >)));
}

#ifdef USE_ZEROCONF
void ConnectDialog::onResolved(const BonjourRecord record, const QString host, const uint16_t port) {
	qlBonjourActive.removeAll(record);
	foreach (ServerItem *si, qlItems) {
		if (si->zeroconfRecord == record) {
			unsigned short usport = static_cast< unsigned short >(port);
			if ((host != si->qsHostname) || (usport != si->usPort)) {
				stopDns(si);
				si->usPort     = static_cast< unsigned short >(port);
				si->qsHostname = host;
				startDns(si);
			}
		}
	}
}

void ConnectDialog::onUpdateLanList(const QList< BonjourRecord > &list) {
	QSet< ServerItem * > items;
#	if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
	QSet< ServerItem * > old =
		QSet< ServerItem * >(qtwServers->siLAN->qlChildren.begin(), qtwServers->siLAN->qlChildren.end());
#	else
	// In Qt 5.14 QList::toSet() has been deprecated as there exists a dedicated constructor of QSet for this now
	QSet< ServerItem * > old = qtwServers->siLAN->qlChildren.toSet();
#	endif

	foreach (const BonjourRecord &record, list) {
		bool found = false;
		foreach (ServerItem *si, old) {
			if (si->zeroconfRecord == record) {
				items.insert(si);
				found = true;
				break;
			}
		}
		if (!found) {
			ServerItem *si = new ServerItem(record);
			qlItems << si;
			Global::get().zeroconf->startResolver(record);
			startDns(si);
			qtwServers->siLAN->addServerItem(si);
		}
	}
	QSet< ServerItem * > remove = old.subtract(items);
	foreach (ServerItem *si, remove) {
		stopDns(si);
		qlItems.removeAll(si);
		delete si;
	}
}

void ConnectDialog::onLanResolveError(const BonjourRecord record) {
	qlBonjourActive.removeAll(record);
}
#endif

void ConnectDialog::fillList() {
	QList< QTreeWidgetItem * > ql;
	QList< QTreeWidgetItem * > qlNew;

	foreach (const PublicInfo &pi, qlPublicServers) {
		bool found = false;
		foreach (ServerItem *si, qlItems) {
			if ((pi.qsIp == si->qsHostname) && (pi.usPort == si->usPort)) {
				si->qsCountry       = pi.qsCountry;
				si->qsCountryCode   = pi.qsCountryCode;
				si->qsContinentCode = pi.qsContinentCode;
				si->qsUrl           = pi.quUrl.toString();
				si->bCA             = pi.bCA;
				si->setDatas();

				if (si->itType == ServerItem::PublicType)
					found = true;
			}
		}
		if (!found)
			ql << new ServerItem(pi);
	}

	while (!ql.isEmpty()) {
		ServerItem *si = static_cast< ServerItem * >(ql.takeAt(QRandomGenerator::global()->bounded(0, ql.count())));
		qlNew << si;
		qlItems << si;
	}

	foreach (QTreeWidgetItem *qtwi, qlNew) {
		ServerItem *si = static_cast< ServerItem * >(qtwi);
		qtwServers->siPublic->addServerItem(si);
		filterServer(si);
		startDns(si);
	}
}

void ConnectDialog::timeTick() {
	if (!bLastFound && !Global::get().s.qsLastServer.isEmpty()) {
		QList< QTreeWidgetItem * > items =
			qtwServers->findItems(Global::get().s.qsLastServer, Qt::MatchExactly | Qt::MatchRecursive);
		if (!items.isEmpty()) {
			bLastFound = true;
			qtwServers->setCurrentItem(items.at(0));
			if (Global::get().s.bAutoConnect && bAutoConnect) {
				siAutoConnect = static_cast< ServerItem * >(items.at(0));
				if (!siAutoConnect->qlAddresses.isEmpty()) {
					accept();
					return;
				} else if (!bAllowHostLookup) {
					accept();
					return;
				}
			}
		}
	}

	if (bAllowHostLookup) {
		// Start DNS Lookup of first unknown hostname
		foreach (const UnresolvedServerAddress &unresolved, qlDNSLookup) {
			if (qsDNSActive.contains(unresolved)) {
				continue;
			}

			qlDNSLookup.removeAll(unresolved);
			qlDNSLookup.append(unresolved);

			qsDNSActive.insert(unresolved);
			ServerResolver *sr = new ServerResolver();
			QObject::connect(sr, SIGNAL(resolved()), this, SLOT(lookedUp()));
			sr->resolve(unresolved.hostname, unresolved.port);
			break;
		}
	}

	ServerItem *current = static_cast< ServerItem * >(qtwServers->currentItem());
	ServerItem *hover =
		static_cast< ServerItem * >(qtwServers->itemAt(qtwServers->viewport()->mapFromGlobal(QCursor::pos())));

	ServerItem *si = nullptr;

	if (tCurrent.elapsed() >= 1000000ULL)
		si = current;
	if (!si && (tHover.elapsed() >= 1000000ULL))
		si = hover;

	if (si) {
		QString hostname    = si->qsHostname.toLower();
		unsigned short port = si->usPort;
		UnresolvedServerAddress unresolved(hostname, port);

		if (si->qlAddresses.isEmpty()) {
			if (!hostname.isEmpty()) {
				qlDNSLookup.removeAll(unresolved);
				qlDNSLookup.prepend(unresolved);
			}
			si = nullptr;
		}
	}

	if (!si) {
		if (qlItems.isEmpty())
			return;

		bool expanded;

		do {
			++iPingIndex;
			if (iPingIndex >= qlItems.count()) {
				if (tRestart.isElapsed(1000000ULL))
					iPingIndex = 0;
				else
					return;
			}
			si = qlItems.at(iPingIndex);

			ServerItem *p = si->siParent;
			expanded      = true;
			while (p && expanded) {
				expanded = expanded && p->isExpanded();
				p        = p->siParent;
			}
		} while (si->qlAddresses.isEmpty() || !expanded);
	}

	if (si == current)
		tCurrent.restart();
	if (si == hover)
		tHover.restart();

	for (const ServerAddress &addr : si->qlAddresses) {
		sendPing(addr.host.toAddress(), addr.port, si->m_version);
	}
}

void ConnectDialog::filterPublicServerList() const {
	if (!Global::get().s.bDisablePublicList) {
		foreach (ServerItem *const si, qtwServers->siPublic->qlChildren) { filterServer(si); }
	}
}

void ConnectDialog::filterServer(ServerItem *const si) const {
	if (!si->qsName.contains(qsSearchServername, Qt::CaseInsensitive)) {
		si->setHidden(true);
		return;
	}
	if (qsSearchLocation != QLatin1String("all")) {
		if (qsSearchLocation != si->qsCountry && qsSearchLocation != si->qsContinentCode) {
			si->setHidden(true);
			return;
		}
	}
	if (Global::get().s.ssFilter == Settings::ShowReachable && si->dPing == 0.0) {
		si->setHidden(true);
		return;
	} else if (Global::get().s.ssFilter == Settings::ShowPopulated && si->uiUsers == 0) {
		si->setHidden(true);
		return;
	}
	si->setHidden(false);
}

void ConnectDialog::addCountriesToSearchLocation() const {
	QMap< QString, QString > qmCountries;

	foreach (const PublicInfo &pi, qlPublicServers) {
		if (pi.qsCountry != tr("Unknown") && !qmCountries.contains(pi.qsCountry)) {
			qmCountries.insert(pi.qsCountry, pi.qsCountryCode);
		}
	}

	foreach (auto location, qmCountries.keys()) {
		// Set Icon, Text and Data
		qcbSearchLocation->addItem(
			ServerItem::loadIcon(QString::fromLatin1(":/flags/%1.svg").arg(qmCountries.value(location))), location,
			location);
	}
}

void ConnectDialog::startDns(ServerItem *si) {
	if (!bAllowHostLookup) {
		return;
	}

	QString hostname    = si->qsHostname.toLower();
	unsigned short port = si->usPort;
	UnresolvedServerAddress unresolved(hostname, port);

	if (si->qlAddresses.isEmpty()) {
		// Determine if qsHostname is an IP address
		// or a hostname. If it is an IP address, we
		// can treat it as resolved as-is.
		QHostAddress qha(si->qsHostname);
		bool hostnameIsIPAddress = !qha.isNull();
		if (hostnameIsIPAddress) {
			si->qlAddresses.append(ServerAddress(HostAddress(qha), port));
		} else {
			si->qlAddresses = qhDNSCache.value(unresolved);
		}
	}

	if (qtwServers->currentItem() == si)
		qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!si->qlAddresses.isEmpty());

	if (!si->qlAddresses.isEmpty()) {
		foreach (const ServerAddress &addr, si->qlAddresses) { qhPings[addr].insert(si); }
		return;
	}
#ifdef USE_ZEROCONF
	if (bAllowZeroconf && si->qsHostname.isEmpty() && !si->zeroconfRecord.serviceName.isEmpty()) {
		if (!qlBonjourActive.contains(si->zeroconfRecord)) {
			Global::get().zeroconf->startResolver(si->zeroconfRecord);
			qlBonjourActive.append(si->zeroconfRecord);
		}
		return;
	}
#endif
	if (!qhDNSWait.contains(unresolved)) {
		if (si->itType == ServerItem::PublicType)
			qlDNSLookup.append(unresolved);
		else
			qlDNSLookup.prepend(unresolved);
	}
	qhDNSWait[unresolved].insert(si);
}

void ConnectDialog::stopDns(ServerItem *si) {
	if (!bAllowHostLookup) {
		return;
	}

	foreach (const ServerAddress &addr, si->qlAddresses) {
		if (qhPings.contains(addr)) {
			qhPings[addr].remove(si);
			if (qhPings[addr].isEmpty()) {
				qhPings.remove(addr);
				qhPingRand.remove(addr);
			}
		}
	}

	QString hostname    = si->qsHostname.toLower();
	unsigned short port = si->usPort;
	UnresolvedServerAddress unresolved(hostname, port);

	if (qhDNSWait.contains(unresolved)) {
		qhDNSWait[unresolved].remove(si);
		if (qhDNSWait[unresolved].isEmpty()) {
			qhDNSWait.remove(unresolved);
			qlDNSLookup.removeAll(unresolved);
		}
	}
}

void ConnectDialog::lookedUp() {
	ServerResolver *sr = qobject_cast< ServerResolver * >(QObject::sender());
	sr->deleteLater();

	QString hostname    = sr->hostname().toLower();
	unsigned short port = sr->port();
	UnresolvedServerAddress unresolved(hostname, port);

	qsDNSActive.remove(unresolved);

	// An error occurred, or no records were found.
	if (sr->records().size() == 0) {
		return;
	}

	QSet< ServerAddress > qs;
	foreach (ServerResolverRecord record, sr->records()) {
		foreach (const HostAddress &ha, record.addresses()) { qs.insert(ServerAddress(ha, record.port())); }
	}

	QSet< ServerItem * > waiting = qhDNSWait[unresolved];
	foreach (ServerItem *si, waiting) {
		foreach (const ServerAddress &addr, qs) { qhPings[addr].insert(si); }

		si->qlAddresses = qs.values();
	}

	qlDNSLookup.removeAll(unresolved);
	qhDNSCache.insert(unresolved, qs.values());
	qhDNSWait.remove(unresolved);

	foreach (ServerItem *si, waiting) {
		if (si == qtwServers->currentItem()) {
			on_qtwServers_currentItemChanged(si, si);
			if (si == siAutoConnect)
				accept();
		}
	}

	if (bAllowPing) {
		for (const ServerAddress &addr : qs) {
			sendPing(addr.host.toAddress(), addr.port, Version::UNKNOWN);
		}
	}
}

void ConnectDialog::sendPing(const QHostAddress &host, unsigned short port, Version::full_t protocolVersion) {
	ServerAddress addr(HostAddress(host), port);

	quint64 uiRand;
	if (qhPingRand.contains(addr)) {
		uiRand = qhPingRand.value(addr);
	} else {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
		uiRand = QRandomGenerator::global()->generate64() << 32;
#else
		// Qt 5.10 introduces the QRandomGenerator class and in Qt 5.15 qrand got deprecated in its favor
		uiRand = (static_cast< quint64 >(qrand()) << 32) | static_cast< quint64 >(qrand());
#endif
		qhPingRand.insert(addr, uiRand);
	}

	Mumble::Protocol::PingData pingData;
	// "Encrypt" the timestamp so that server's can't spoof the returned timestamp (easily) to fake a better ping
	pingData.timestamp                    = tPing.elapsed() ^ uiRand;
	pingData.requestAdditionalInformation = true;

	if (!writePing(host, port, protocolVersion, pingData)) {
		return;
	}
	if (protocolVersion == Version::UNKNOWN) {
		// Also attempt to use new ping format in case we are pinging a server that only knows the new format
		writePing(host, port, Mumble::Protocol::PROTOBUF_INTRODUCTION_VERSION, pingData);
	}


	const QSet< ServerItem * > &qs = qhPings.value(addr);

	foreach (ServerItem *si, qs)
		++si->uiSent;
}

bool ConnectDialog::writePing(const QHostAddress &host, unsigned short port, Version::full_t protocolVersion,
							  const Mumble::Protocol::PingData &pingData) {
	m_udpPingEncoder.setProtocolVersion(protocolVersion);

	gsl::span< const Mumble::Protocol::byte > encodedPacket = m_udpPingEncoder.encodePingPacket(pingData);

	if (bIPv4 && host.protocol() == QAbstractSocket::IPv4Protocol) {
		qusSocket4->writeDatagram(reinterpret_cast< const char * >(encodedPacket.data()),
								  static_cast< qint64 >(encodedPacket.size()), host, port);
	} else if (bIPv6 && host.protocol() == QAbstractSocket::IPv6Protocol) {
		qusSocket6->writeDatagram(reinterpret_cast< const char * >(encodedPacket.data()),
								  static_cast< qint64 >(encodedPacket.size()), host, port);
	} else {
		return false;
	}

	return true;
}

void ConnectDialog::udpReply() {
	QUdpSocket *sock = qobject_cast< QUdpSocket * >(sender());

	while (sock->hasPendingDatagrams()) {
		QHostAddress host;
		unsigned short port;

		gsl::span< Mumble::Protocol::byte > buffer = m_udpDecoder.getBuffer();

		std::size_t len = static_cast< std::size_t >(sock->readDatagram(
			reinterpret_cast< char * >(buffer.data()), static_cast< int >(buffer.size()), &host, &port));

		// Pings are special in that they can be decoded in the new or the old format, if the protocol version is set to
		// the old format (which UNKNOWN does). Thus by setting the version to UNKNOWN, we effectively enable to decode
		// either format. We have to reset it to this value every time, since the call to decode may set the protocol
		// version to a more recent version (if a ping in new format is detected).
		m_udpDecoder.setProtocolVersion(Version::UNKNOWN);

		if (m_udpDecoder.decodePing(buffer.subspan(0, len))
			&& m_udpDecoder.getMessageType() == Mumble::Protocol::UDPMessageType::Ping) {
			if (host.scopeId() == QLatin1String("0"))
				host.setScopeId(QLatin1String(""));

			ServerAddress address(HostAddress(host), port);

			if (qhPings.contains(address)) {
				Mumble::Protocol::PingData pingData = m_udpDecoder.getPingData();

				quint64 elapsed = tPing.elapsed() - (pingData.timestamp ^ qhPingRand.value(address));

				for (ServerItem *si : qhPings.value(address)) {
					si->m_version    = pingData.serverVersion;
					quint32 users    = pingData.userCount;
					quint32 maxusers = pingData.maxUserCount;
					si->uiBandwidth  = pingData.maxBandwidthPerUser;

					if (!si->uiPingSort)
						si->uiPingSort = qmPingCache.value(UnresolvedServerAddress(si->qsHostname, si->usPort));

					si->setDatas(static_cast< double >(elapsed), users, maxusers);
					if (si->itType == ServerItem::PublicType) {
						filterServer(si);
					}
				}
			}
		}
	}
}

void ConnectDialog::fetched(QByteArray xmlData, QUrl, QMap< QString, QString > headers) {
	if (xmlData.isNull()) {
		QMessageBox::warning(this, QLatin1String("Mumble"), tr("Failed to fetch server list"), QMessageBox::Ok);
		return;
	}

	QDomDocument doc;
	doc.setContent(xmlData);

	qlPublicServers.clear();
	qsUserCountry       = headers.value(QLatin1String("Geo-Country"));
	qsUserCountryCode   = headers.value(QLatin1String("Geo-Country-Code")).toLower();
	qsUserContinentCode = headers.value(QLatin1String("Geo-Continent-Code")).toLower();

	QDomElement root = doc.documentElement();
	QDomNode n       = root.firstChild();
	while (!n.isNull()) {
		QDomElement e = n.toElement();
		if (!e.isNull()) {
			if (e.tagName() == QLatin1String("server")) {
				PublicInfo pi;
				pi.qsName          = e.attribute(QLatin1String("name"));
				pi.quUrl           = e.attribute(QLatin1String("url"));
				pi.qsIp            = e.attribute(QLatin1String("ip"));
				pi.usPort          = e.attribute(QLatin1String("port")).toUShort();
				pi.qsCountry       = e.attribute(QLatin1String("country"), tr("Unknown"));
				pi.qsCountryCode   = e.attribute(QLatin1String("country_code")).toLower();
				pi.qsContinentCode = e.attribute(QLatin1String("continent_code")).toLower();
				pi.bCA             = e.attribute(QLatin1String("ca")).toInt() ? true : false;

				qlPublicServers << pi;
			}
		}
		n = n.nextSibling();
	}
	addCountriesToSearchLocation();
	tPublicServers.restart();

	fillList();
}

void ConnectDialog::on_qleSearchServername_textChanged(const QString &searchServername) {
	qsSearchServername = searchServername;
	filterPublicServerList();
}

void ConnectDialog::on_qcbSearchLocation_currentIndexChanged(int searchLocationIndex) {
	qsSearchLocation = qcbSearchLocation->itemData(searchLocationIndex).toString();
	filterPublicServerList();
}

void ConnectDialog::on_qcbFilter_currentIndexChanged(int filterIndex) {
	const QString filter = qcbFilter->itemText(filterIndex);
	if (filter == tr("Show All")) {
		Global::get().s.ssFilter = Settings::ShowAll;
	} else if (filter == tr("Show Reachable")) {
		Global::get().s.ssFilter = Settings::ShowReachable;
	} else if (filter == tr("Show Populated")) {
		Global::get().s.ssFilter = Settings::ShowPopulated;
	}

	filterPublicServerList();
}
