#include <mdns/MdnsBrowser.h>
#include <qmdnsengine/message.h>
#include <qmdnsengine/service.h>

//Qt includes
#include <QThread>

#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QHostAddress>
#include <QRegularExpression>

// Utility includes
#include <HyperionConfig.h>
#include <utils/Logger.h>
#include <utils/WaitTime.h>
#include <utils/NetUtils.h>

namespace {
	const bool verboseBrowser = false;
} //End of constants

MdnsBrowser::MdnsBrowser(QObject* parent)
	: QObject(parent)
	, _log(Logger::getInstance("MDNS"))
{
	qRegisterMetaType<QHostAddress>("QHostAddress");
}

MdnsBrowser::~MdnsBrowser()
{
	qDeleteAll(_browsedServiceTypes);
}

void MdnsBrowser::browseForServiceType(const QByteArray& serviceType)
{
	if (!_browsedServiceTypes.contains(serviceType))
	{
		DebugIf(verboseBrowser, _log, "Start new mDNS browser for serviceType [%s], Thread: %s", serviceType.constData(), QSTRING_CSTR(QThread::currentThread()->objectName()));
		QMdnsEngine::Browser* newBrowser = new QMdnsEngine::Browser(&_server, serviceType, &_cache);

		QObject::connect(newBrowser, &QMdnsEngine::Browser::serviceAdded, this, &MdnsBrowser::onServiceAdded);
		QObject::connect(newBrowser, &QMdnsEngine::Browser::serviceUpdated, this, &MdnsBrowser::onServiceUpdated);
		QObject::connect(newBrowser, &QMdnsEngine::Browser::serviceRemoved, this, &MdnsBrowser::onServiceRemoved);

		_browsedServiceTypes.insert(serviceType, newBrowser);
	}
	else
	{
		DebugIf(verboseBrowser, _log, "Use existing mDNS browser for serviceType [%s], Thread: %s", serviceType.constData(), QSTRING_CSTR(QThread::currentThread()->objectName()));
	}
}

void MdnsBrowser::onServiceAdded(const QMdnsEngine::Service& service)
{
	DebugIf(verboseBrowser, _log, "Discovered service [%s] at host: %s, port: %u, Thread: %s", service.name().constData(), service.hostname().constData(), service.port(), QSTRING_CSTR(QThread::currentThread()->objectName()));
	emit serviceFound(service);
}

void MdnsBrowser::onServiceUpdated(const QMdnsEngine::Service& service)
{
	DebugIf(verboseBrowser, _log, "[%s], Name: [%s], Port: [%u], Thread: %s", service.type().constData(), service.name().constData(), service.port(), QSTRING_CSTR(QThread::currentThread()->objectName()));
}

void MdnsBrowser::onServiceRemoved(const QMdnsEngine::Service& service)
{
	DebugIf(verboseBrowser, _log, "[%s], Name: [%s], Port: [%u], Thread: %s", service.type().constData(), service.name().constData(), service.port(), QSTRING_CSTR(QThread::currentThread()->objectName()));
	emit serviceRemoved(service);
}

QHostAddress MdnsBrowser::getHostFirstAddress(const QByteArray& hostname)
{
	DebugIf(verboseBrowser, _log, "for hostname [%s], Thread: %s", hostname.constData(), QSTRING_CSTR(QThread::currentThread()->objectName()));
	QByteArray toBeResolvedHostName {hostname};

	QHostAddress hostAddress;

	if (toBeResolvedHostName.endsWith(".local"))
	{
		toBeResolvedHostName.append('.');
	}
	if (toBeResolvedHostName.endsWith(".local."))
	{
		QList<QMdnsEngine::Record> aRecords;
		if (_cache.lookupRecords(toBeResolvedHostName, QMdnsEngine::A, aRecords))
		{
			foreach(QMdnsEngine::Record record, aRecords)
			{
				// Do not publish link local addresses
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
				if (!record.address().isLinkLocal())
#else
				if (!record.address().toString().startsWith("fe80"))
#endif
				{
					hostAddress = record.address();
					DebugIf(verboseBrowser, _log, "Hostname [%s] translates to IPv4-address [%s]", toBeResolvedHostName.constData(), QSTRING_CSTR(hostAddress.toString()));
					break;
				}
			}
		}
		else
		{
			QList<QMdnsEngine::Record> aaaaRecords;
			if (_cache.lookupRecords(toBeResolvedHostName, QMdnsEngine::AAAA, aaaaRecords))
			{
				foreach(QMdnsEngine::Record record, aaaaRecords)
				{
					// Do not publish link local addresses
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
					if (!record.address().isLinkLocal())
#else
					if (!record.address().toString().startsWith("fe80"))
#endif
					{
						hostAddress = record.address();
						DebugIf(verboseBrowser, _log, "Hostname [%s] translates to IPv6-address [%s]", toBeResolvedHostName.constData(), QSTRING_CSTR(hostAddress.toString()));
						break;
					}
				}
			}
			else
			{
				DebugIf(verboseBrowser, _log, "IP-address for hostname [%s] not yet in cache, start resolver.", toBeResolvedHostName.constData());
				qRegisterMetaType<QMdnsEngine::Message>("Message");
				auto* resolver = new QMdnsEngine::Resolver(&_server, toBeResolvedHostName, &_cache);
				connect(resolver, &QMdnsEngine::Resolver::resolved, this, &MdnsBrowser::onHostNameResolved);
			}
		}
	}
	return hostAddress;
}

void MdnsBrowser::onHostNameResolved(const QHostAddress& address)
{
	DebugIf(verboseBrowser, _log, "for address [%s], Thread: %s", QSTRING_CSTR(address.toString()), QSTRING_CSTR(QThread::currentThread()->objectName()));

	// Do not publish link local addresses
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
	if (!address.isLinkLocal())
#else
	if (!address.toString().startsWith("fe80"))
#endif
	{
		emit addressResolved(address);
	}
}

bool MdnsBrowser::resolveAddress(Logger* log, const QString& hostname, QHostAddress& hostAddress, std::chrono::milliseconds timeout)
{
	DebugIf(verboseBrowser, _log, "Get address for hostname [%s], Thread: %s", QSTRING_CSTR(hostname), QSTRING_CSTR(QThread::currentThread()->objectName()));

	bool isHostAddressOK{ false };
	if (hostname.endsWith(".local") || hostname.endsWith(".local."))
	{
		hostAddress = getHostFirstAddress(hostname.toUtf8());

		if (hostAddress.isNull())
		{
			DebugIf(verboseBrowser, _log, "Wait for resolver on hostname [%s]", QSTRING_CSTR(hostname));

			QEventLoop loop;
			QTimer t;
			QObject::connect(&MdnsBrowser::getInstance(), &MdnsBrowser::addressResolved, &loop, &QEventLoop::quit);

			weakConnect(&MdnsBrowser::getInstance(), &MdnsBrowser::addressResolved,
				[&hostAddress, hostname](const QHostAddress& resolvedAddress) {
					DebugIf(verboseBrowser, Logger::getInstance("MDNS"), "Resolver resolved hostname [%s] to address [%s], Thread: %s", QSTRING_CSTR(hostname), QSTRING_CSTR(resolvedAddress.toString()), QSTRING_CSTR(QThread::currentThread()->objectName()));
					hostAddress = resolvedAddress;
				});

			QTimer::connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit);
			t.start(static_cast<int>(timeout.count()));
			loop.exec();
		}

		if (!hostAddress.isNull())
		{
			Debug(log, "Resolved mDNS hostname [%s] to address [%s]", QSTRING_CSTR(hostname), QSTRING_CSTR(hostAddress.toString()));
			isHostAddressOK = true;
		}
		else
		{
			QObject::disconnect(&MdnsBrowser::getInstance(), &MdnsBrowser::addressResolved, nullptr, nullptr);
			Error(log, "Resolved mDNS hostname [%s] timed out", QSTRING_CSTR(hostname));
		}
	}
	else
	{
		Error(log, "Hostname [%s] is not an mDNS hostname.", QSTRING_CSTR(hostname));
		isHostAddressOK = false;
	}
	return isHostAddressOK;
}

QMdnsEngine::Record MdnsBrowser::getServiceInstanceRecord(const QByteArray& serviceInstance, const std::chrono::milliseconds waitTime) const
{
	DebugIf(verboseBrowser, _log, "Get service instance [%s] details, Thread: %s",serviceInstance.constData(), QSTRING_CSTR(QThread::currentThread()->objectName()));

	QByteArray service{ serviceInstance };

	if (!service.endsWith('.'))
	{
		service.append('.');
	}

	QMdnsEngine::Record srvRecord;
	bool found{ false };
	int retries = 5;
	do
	{
		if (_cache.lookupRecord(service, QMdnsEngine::SRV, srvRecord))
		{
			found = true;
		}
		else
		{
			wait(waitTime);
			--retries;
		}

	} while (!found && retries >= 0);

	if (found)
	{
		DebugIf(verboseBrowser, _log, "Service record found for service instance [%s]", service.constData());

	}
	else
	{
		Debug(_log, "No service record found for service instance [%s]", service.constData());
	}
	return srvRecord;
}

QMdnsEngine::Service MdnsBrowser::getFirstService(const QByteArray& serviceType, const QString& filter, const std::chrono::milliseconds waitTime) const
{
	DebugIf(verboseBrowser,_log, "Get first service of type [%s], matching name: [%s]", QSTRING_CSTR(QString(serviceType)), QSTRING_CSTR(filter));

	QMdnsEngine::Service service;

	QRegularExpression regEx(filter);
	if (!regEx.isValid()) {
		QString errorString = regEx.errorString();
		int errorOffset = regEx.patternErrorOffset();

		Error(_log, "Filtering regular expression [%s] error [%d]:[%s]", QSTRING_CSTR(filter), errorOffset, QSTRING_CSTR(errorString));
	}
	else
	{
		QList<QMdnsEngine::Record> ptrRecords;

		bool found {false};
		int retries = 3;
		do
		{
			if (_cache.lookupRecords(serviceType, QMdnsEngine::PTR, ptrRecords))
			{
				for (int ptrCounter = 0; ptrCounter < ptrRecords.size(); ++ptrCounter)
				{
					QByteArray serviceNameFull = ptrRecords.at(ptrCounter).target();

					QRegularExpressionMatch match = regEx.match(serviceNameFull.constData());
					if (match.hasMatch())
					{
						QMdnsEngine::Record srvRecord;
						if (!_cache.lookupRecord(serviceNameFull, QMdnsEngine::SRV, srvRecord))
						{
							DebugIf(verboseBrowser, _log, "No SRV record for [%s] found, skip entry", serviceNameFull.constData());
						}
						else
						{
							if (serviceNameFull.endsWith("." + serviceType))
							{
								service.setName(serviceNameFull.left(serviceNameFull.length() - serviceType.length() - 1));
							}
							else
							{
								service.setName(srvRecord.name());
							}
							service.setPort(srvRecord.port());

							QByteArray hostName = srvRecord.target();
							//Remove trailing dot
							hostName.chop(1);
							service.setHostname(hostName);
							service.setAttributes(srvRecord.attributes());
							found = true;
						}
					}
				}
			}
			else
			{
				wait(waitTime);
				--retries;
			}

		} while (!found && retries >= 0);

		if (found)
		{
			DebugIf(verboseBrowser,_log, "Service of type [%s] found", serviceType.constData());
		}
		else
		{
			Debug(_log, "No service of type [%s] found", serviceType.constData());

		}
	}

	return service;
}

QJsonArray MdnsBrowser::getServicesDiscoveredJson(const QByteArray& serviceType, const QString& filter, const std::chrono::milliseconds waitTime) const
{
	DebugIf(verboseBrowser,_log, "Get services of type [%s], matching name: [%s], Thread: %s", QSTRING_CSTR(QString(serviceType)), QSTRING_CSTR(filter), QSTRING_CSTR(QThread::currentThread()->objectName()));

	QJsonArray result;

	QRegularExpression regEx(filter);
	if (!regEx.isValid()) {
		QString errorString = regEx.errorString();
		int errorOffset = regEx.patternErrorOffset();

		Error(_log, "Filtering regular expression [%s] error [%d]:[%s]", QSTRING_CSTR(filter), errorOffset, QSTRING_CSTR(errorString));
	}
	else
	{
		QList<QMdnsEngine::Record> ptrRecords;

		int retries = 3;
		do
		{
			if (_cache.lookupRecords(serviceType, QMdnsEngine::PTR, ptrRecords))
			{
				for (int ptrCounter = 0; ptrCounter < ptrRecords.size(); ++ptrCounter)
				{
					QByteArray serviceName = ptrRecords.at(ptrCounter).target();

					QRegularExpressionMatch match = regEx.match(serviceName.constData());
					if (match.hasMatch())
					{
						QMdnsEngine::Record srvRecord;
						if (!_cache.lookupRecord(serviceName, QMdnsEngine::SRV, srvRecord))
						{
							Debug(_log, "No SRV record for [%s] found, skip entry", serviceName.constData());
						}
						else
						{
							QJsonObject obj;
							QString domain = "local.";

							obj.insert("id", serviceName.constData());

							QString service = serviceName;
							service.chop(1);
							obj.insert("service", service);
							obj.insert("type", serviceType.constData());

							QString name;
							if (serviceName.endsWith("." + serviceType))
							{
								name = serviceName.left(serviceName.length() - serviceType.length() - 1);
								obj.insert("name", QString(name));
							}

							QByteArray hostName = srvRecord.target();
							//Remove trailing dot
							hostName.chop(1);

							obj.insert("hostname", QString(hostName));
							obj.insert("domain", domain);

							//Tag records where the service is provided by this host
							QByteArray localHostname = QHostInfo::localHostName().toUtf8();
							localHostname = localHostname.replace('.', '-');

							bool isSameHost {false};
							if ( name == localHostname )
							{
								isSameHost = true;
							}
							obj.insert("sameHost", isSameHost);

							quint16 port = srvRecord.port();
							obj.insert("port", port);

							QMdnsEngine::Record txtRecord;
							if (_cache.lookupRecord(serviceName, QMdnsEngine::TXT, txtRecord))
							{
								QMap<QByteArray, QByteArray> txtAttributes = txtRecord.attributes();

								QVariantMap txtMap;
								QMapIterator<QByteArray, QByteArray> i(txtAttributes);
								while (i.hasNext()) {
									i.next();
									txtMap.insert(i.key(), i.value());
								}
								obj.insert("txt", QJsonObject::fromVariantMap(txtMap));
							}
							result << obj;
						}
					}
				}
			}

			if ( result.isEmpty())
			{
				wait(waitTime);
				--retries;
			}

		} while (result.isEmpty() && retries >= 0);

		if (!result.isEmpty())
		{
			DebugIf(verboseBrowser,_log, "result: [%s]", QString(QJsonDocument(result).toJson(QJsonDocument::Compact)).toUtf8().constData());
		}
		else
		{
			Debug(_log, "No service of type [%s] found", serviceType.constData());

		}
	}

	return result;
}

void MdnsBrowser::printCache(const QByteArray& name, quint16 type) const
{
	DebugIf(verboseBrowser,_log, "for type: %s", QSTRING_CSTR(QMdnsEngine::typeName(type)));
	QList<QMdnsEngine::Record> records;
	if (_cache.lookupRecords(name, type, records))
	{
		qDebug() << "";
		foreach(QMdnsEngine::Record record, records)
		{
			qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << "], ttl       : " << record.ttl();

			switch (record.type()) {
			case QMdnsEngine::PTR:
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", target    : " << record.target();
				break;

			case QMdnsEngine::SRV:
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", target    : " << record.target();
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", port      : " << record.port();
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", priority  : " << record.priority();
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", weight    : " << record.weight();
				break;
			case QMdnsEngine::TXT:
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", attributes: " << record.attributes();
				break;

			case QMdnsEngine::NSEC:
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", nextDomNam: " << record.nextDomainName();
				break;

			case QMdnsEngine::A:
			case QMdnsEngine::AAAA:
				qDebug() << QMdnsEngine::typeName(record.type()) << "," << record.name() << ", address   : " << record.address();
				break;
			}
		}
	}
	else
	{
		DebugIf(verboseBrowser,_log, "Cash is empty for type: %s", QSTRING_CSTR(QMdnsEngine::typeName(type)));
	}
}
