/**************************************************************************************************
---------------------------------------------------------------------------------------------------
	Copyright (C) 2015  Jonathan Bagg
	This file is part of QtZeroConf.

	QtZeroConf is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	QtZeroConf is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with QtZeroConf.  If not, see <http://www.gnu.org/licenses/>.
---------------------------------------------------------------------------------------------------
   Project name : QtZeroConf
   File name    : bonjour.cpp
   Created      : 20 July 2015
   Author(s)    : Jonathan Bagg
---------------------------------------------------------------------------------------------------
   Wrapper for Apple's Bonjour library for use on Windows, MACs and iOS
---------------------------------------------------------------------------------------------------
**************************************************************************************************/
#include "qzeroconf.h"
#include "bonjour_p.h"



void Resolver::resolverReady()
{
	DNSServiceErrorType err = DNSServiceProcessResult(DNSresolverRef);
	if (err != kDNSServiceErr_NoError)
		cleanUp();
}

void Resolver::addressReady()
{
	DNSServiceErrorType err = DNSServiceProcessResult(DNSaddressRef);
	if (err != kDNSServiceErr_NoError)
		cleanUp();
}

void Resolver::cleanUp()
{
	DNSServiceRefDeallocate(DNSresolverRef);
	DNSServiceRefDeallocate(DNSaddressRef);
	// the QSocketNotifiers resolverNotifier and addressNotifier get deleted when the QSharedPointer gets deleted along with the Resolver.  No need to clear them here.
	QString key = zcs->name() + QString::number(zcs->interfaceIndex());
	ref->resolvers.remove(key);
	delete this;
}

QZeroConfPrivate::QZeroConfPrivate(QZeroConf *parent)
{
	pub = parent;
}

void QZeroConfPrivate::bsRead()
{
	DNSServiceErrorType err = DNSServiceProcessResult(dnssRef);
	if (err != kDNSServiceErr_NoError) {
		cleanUp(dnssRef);
		emit pub->error(QZeroConf::serviceRegistrationFailed);
	}
}

void QZeroConfPrivate::browserRead()
{
	DNSServiceErrorType err = DNSServiceProcessResult(browser);
	if (err != kDNSServiceErr_NoError) {
		cleanUp(browser);
		emit pub->error(QZeroConf::browserFailed);
	}
}

void QZeroConfPrivate::resolve(QZeroConfService zcs)
{
	DNSServiceErrorType err;
	Resolver *resolver = new Resolver;
	QString key = zcs->name() + QString::number(zcs->interfaceIndex());
	resolvers.insert(key, resolver);
	resolver->ref = this;
	resolver->zcs = zcs;

	err = DNSServiceResolve(&resolver->DNSresolverRef, kDNSServiceFlagsTimeout, zcs->interfaceIndex(), zcs->name().toUtf8(), zcs->type().toUtf8(), zcs->domain().toUtf8(), static_cast<DNSServiceResolveReply>(resolverCallback), resolver);
	if (err == kDNSServiceErr_NoError) {
		int sockfd = DNSServiceRefSockFD(resolver->DNSresolverRef);
		if (sockfd == -1) {
			resolver->cleanUp();
		}
		else {
			resolver->resolverNotifier = QSharedPointer<QSocketNotifier>::create(sockfd, QSocketNotifier::Read);
			connect(resolver->resolverNotifier.data(), &QSocketNotifier::activated, resolver, &Resolver::resolverReady);
		}
	}
	else {
		resolver->cleanUp();
	}
}

void DNSSD_API QZeroConfPrivate::registerCallback(DNSServiceRef, DNSServiceFlags, DNSServiceErrorType errorCode, const char *, const char *, const char *, void *userdata)
{
	QZeroConfPrivate *ref = static_cast<QZeroConfPrivate *>(userdata);

	if (errorCode == kDNSServiceErr_NoError) {
		emit ref->pub->servicePublished();
	}
	else {
		ref->cleanUp(ref->dnssRef);
		emit ref->pub->error(QZeroConf::serviceRegistrationFailed);
	}
}

void DNSSD_API QZeroConfPrivate::browseCallback(DNSServiceRef, DNSServiceFlags flags,
		quint32 interfaceIndex, DNSServiceErrorType err, const char *name,
		const char *type, const char *domain, void *userdata)
{
	QString key;
	QZeroConfService zcs;
	QZeroConfPrivate *ref = static_cast<QZeroConfPrivate *>(userdata);

	//qDebug() << name;
	if (err == kDNSServiceErr_NoError) {
		key = name + QString::number(interfaceIndex);
		if (flags & kDNSServiceFlagsAdd) {
			if (!ref->pub->services.contains(key)) {
				zcs = QZeroConfService::create();
				zcs->m_name = name;
				zcs->m_type = type;
				zcs->m_domain = domain;
				zcs->m_interfaceIndex = interfaceIndex;
				ref->resolve(zcs);
			}
		}
		else if (ref->pub->services.contains(key)) {
			zcs = ref->pub->services[key];
			ref->pub->services.remove(key);
			if (ref->resolvers.contains(key))
				ref->resolvers[key]->cleanUp();
			emit ref->pub->serviceRemoved(zcs);
		}
	}
	else {
		ref->cleanUp(ref->browser);
		emit ref->pub->error(QZeroConf::browserFailed);
	}
}

void DNSSD_API QZeroConfPrivate::resolverCallback(DNSServiceRef, DNSServiceFlags,
		quint32 interfaceIndex, DNSServiceErrorType err, const char *,
		const char *hostName, quint16 port, quint16 txtLen,
		const unsigned char *txtRecord, void *userdata)
{
	Resolver *resolver = static_cast<Resolver *>(userdata);

	if (err != kDNSServiceErr_NoError) {
		resolver->cleanUp();
		return;
	}

	uchar recLen;
	while (txtLen > 0)		// add txt records
	{
		recLen = txtRecord[0];
		txtRecord++;
		QByteArray avahiText(reinterpret_cast<const char *>(txtRecord), recLen);
		const int pos = avahiText.indexOf('=');
		if (pos < 0)
			resolver->zcs->m_txt[avahiText] = "";
		else
			resolver->zcs->m_txt[avahiText.left(pos)] = avahiText.mid(pos + 1, -1);

		txtLen-= recLen + 1;
		txtRecord+= recLen;
	}
	resolver->zcs->m_host = hostName;
	resolver->zcs->m_port = qFromBigEndian<quint16>(port);

	if (resolver->DNSaddressRef) {
		DNSServiceRefDeallocate(resolver->DNSaddressRef);
		resolver->DNSaddressRef = nullptr;
	}
	err = DNSServiceGetAddrInfo(&resolver->DNSaddressRef, kDNSServiceFlagsForceMulticast, interfaceIndex, resolver->ref->protocol, hostName, static_cast<DNSServiceGetAddrInfoReply>(addressReply), resolver);
	if (err == kDNSServiceErr_NoError) {
		int sockfd = DNSServiceRefSockFD(resolver->DNSaddressRef);
		if (sockfd == -1) {
			resolver->cleanUp();
		}
		else {
			// Fix "multiple socket notifiers for same socket" warning
			resolver->addressNotifier.clear();
			resolver->addressNotifier = QSharedPointer<QSocketNotifier>::create(sockfd, QSocketNotifier::Read);
			connect(resolver->addressNotifier.data(), &QSocketNotifier::activated, resolver, &Resolver::addressReady);
		}
	}
	else {
		resolver->cleanUp();
	}
}

void DNSSD_API QZeroConfPrivate::addressReply(DNSServiceRef sdRef,
		DNSServiceFlags flags, quint32 interfaceIndex,
		DNSServiceErrorType err, const char *hostName,
		const struct sockaddr* address, quint32 ttl, void *userdata)
{
	Q_UNUSED(interfaceIndex)
	Q_UNUSED(sdRef)
	Q_UNUSED(ttl)
	Q_UNUSED(hostName)

	Resolver *resolver = static_cast<Resolver *>(userdata);

	if (err == kDNSServiceErr_NoError) {
		if ((flags & kDNSServiceFlagsAdd) != 0) {
			QHostAddress hAddress(address);
			resolver->zcs->setIp(hAddress);

			QString key = resolver->zcs->name() + QString::number(interfaceIndex);
			if (!resolver->ref->pub->services.contains(key)) {
				resolver->ref->pub->services.insert(key, resolver->zcs);
				emit resolver->ref->pub->serviceAdded(resolver->zcs);
			}
			else
				emit resolver->ref->pub->serviceUpdated(resolver->zcs);

		}
	}
	else
		resolver->cleanUp();
}

void QZeroConfPrivate::cleanUp(DNSServiceRef toClean)
{
	if (!toClean)
		return;
	else if (toClean == browser) {
		browser = nullptr;
		browserNotifier.clear();
		QMap<QString, QZeroConfService >::iterator i;
		for (i = pub->services.begin(); i != pub->services.end(); i++) {
			QString key = (*i)->name() + QString::number((*i)->interfaceIndex());
			resolvers[key]->cleanUp();
			emit pub->serviceRemoved(*i);
		}
		pub->services.clear();
	}
	else if (toClean == dnssRef) {
		dnssRef = nullptr;
		serviceNotifier.clear();
	}

	DNSServiceRefDeallocate(toClean);
}

QZeroConf::QZeroConf(QObject *parent) : QObject (parent)
{
	pri = new QZeroConfPrivate(this);
	qRegisterMetaType<QZeroConfService>("QZeroConfService");
}

QZeroConf::~QZeroConf()
{
	pri->cleanUp(pri->dnssRef);
	pri->cleanUp(pri->browser);
	delete pri;
}

void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
	DNSServiceErrorType err;

	if (pri->dnssRef) {
		emit error(QZeroConf::serviceRegistrationFailed);
		return;
	}

	err = DNSServiceRegister(&pri->dnssRef, 0, interface,
			name,
			type,
			domain,
			nullptr,
			qFromBigEndian<quint16>(port),
			static_cast<uint16_t>(pri->txt.size()), pri->txt.data(),
			static_cast<DNSServiceRegisterReply>(QZeroConfPrivate::registerCallback), pri);

	if (err == kDNSServiceErr_NoError) {
		int sockfd = DNSServiceRefSockFD(pri->dnssRef);
		if (sockfd == -1) {
			pri->cleanUp(pri->dnssRef);
			emit error(QZeroConf::serviceRegistrationFailed);
		}
		else {
			pri->serviceNotifier = QSharedPointer<QSocketNotifier>::create(sockfd, QSocketNotifier::Read, this);
			connect(pri->serviceNotifier.data(), &QSocketNotifier::activated, pri, &QZeroConfPrivate::bsRead);
		}
	}
	else {
		pri->cleanUp(pri->dnssRef);
		emit error(QZeroConf::serviceRegistrationFailed);
	}
}

void QZeroConf::stopServicePublish(void)
{
	pri->cleanUp(pri->dnssRef);
}

bool QZeroConf::publishExists(void)
{
	if (pri->dnssRef)
		return true;
	else
		return false;
}

void QZeroConf::addServiceTxtRecord(QString nameOnly)
{
	pri->txt.append(static_cast<char>(nameOnly.size()));
	pri->txt.append(nameOnly.toUtf8());
}

void QZeroConf::addServiceTxtRecord(QString name, QString value)
{
	name.append("=");
	name.append(value);
	addServiceTxtRecord(name);
}

void QZeroConf::clearServiceTxtRecords()
{
	pri->txt.clear();
}

void QZeroConf::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol)
{
	DNSServiceErrorType err;

	if (pri->browser) {
		emit error(QZeroConf::browserFailed);
		return;
	}

	switch (protocol) {
		case QAbstractSocket::IPv4Protocol: pri->protocol = kDNSServiceProtocol_IPv4; break;
		case QAbstractSocket::IPv6Protocol: pri->protocol = kDNSServiceProtocol_IPv6; break;
		default:
			qDebug("QZeroConf::startBrowser() - unsupported protocol, using IPv4");
			pri->protocol = kDNSServiceProtocol_IPv4;
			break;
	}

	err = DNSServiceBrowse(&pri->browser, 0, 0, type.toUtf8(), nullptr, static_cast<DNSServiceBrowseReply>(QZeroConfPrivate::browseCallback), pri);
	if (err == kDNSServiceErr_NoError) {
		int sockfd = DNSServiceRefSockFD(pri->browser);
		if (sockfd == -1) {
			pri->cleanUp(pri->browser);
			emit error(QZeroConf::browserFailed);
		}
		else {
			pri->browserNotifier = QSharedPointer<QSocketNotifier>::create(sockfd, QSocketNotifier::Read, this);
			connect(pri->browserNotifier.data(), &QSocketNotifier::activated, pri, &QZeroConfPrivate::browserRead);
		}
	}
	else {
		pri->cleanUp(pri->browser);
		emit error(QZeroConf::browserFailed);
	}
}

void QZeroConf::stopBrowser(void)
{
	pri->cleanUp(pri->browser);
}

bool QZeroConf::browserExists(void)
{
	if (pri->browser)
		return true;
	else
		return false;
}
