/**************************************************************************************************
---------------------------------------------------------------------------------------------------
	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    : avahiclient.cpp
   Created      : 20 July 2015
   Author(s)    : Jonathan Bagg
---------------------------------------------------------------------------------------------------
   Avahi-client wrapper for use in Desktop Linux systems
---------------------------------------------------------------------------------------------------
**************************************************************************************************/
//#include <avahi-qt4/qt-watch.h>	//
#include "avahi-qt/qt-watch.h"		// fixme don't depend on avahi-qt until it supports Qt5
#include <avahi-client/client.h>
#include <avahi-client/publish.h>
#include <avahi-common/error.h>
#include <avahi-client/lookup.h>
#include "qzeroconf.h"

class QZeroConfPrivate
{
public:
	QZeroConfPrivate(QZeroConf *parent)
	{
		qint32 error;

		txt = NULL;
		pub = parent;
		group = NULL;
		browser = NULL;
		poll = avahi_qt_poll_get();
		if (!poll) {
			return;
		}
		client = avahi_client_new(poll, AVAHI_CLIENT_IGNORE_USER_CONFIG, NULL, this, &error);
		if (!client) {
			return;
		}
	}

	static void groupCallback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata)
	{
		QZeroConfPrivate *ref = static_cast<QZeroConfPrivate *>(userdata);
		switch (state) {
			case AVAHI_ENTRY_GROUP_ESTABLISHED:
				emit ref->pub->servicePublished();
				break;
			case AVAHI_ENTRY_GROUP_COLLISION:
				avahi_entry_group_free(g);
				ref->group = NULL;
				emit ref->pub->error(QZeroConf::serviceNameCollision);
				break;
			case AVAHI_ENTRY_GROUP_FAILURE:
				avahi_entry_group_free(g);
				ref->group = NULL;
				emit ref->pub->error(QZeroConf::serviceRegistrationFailed);
				break;
			case AVAHI_ENTRY_GROUP_UNCOMMITED: break;
			case AVAHI_ENTRY_GROUP_REGISTERING: break;
		}
	}

	static void browseCallback(AvahiServiceBrowser *,
			AvahiIfIndex interface,
			AvahiProtocol protocol,
			AvahiBrowserEvent event,
			const char *name,
			const char *type,
			const char *domain,
			AVAHI_GCC_UNUSED AvahiLookupResultFlags flags,
			void* userdata)
	{
		QString key = name + QString::number(interface);
		QZeroConfPrivate *ref = static_cast<QZeroConfPrivate *>(userdata);

		QZeroConfService zcs;

		switch (event) {
		case AVAHI_BROWSER_FAILURE:
			ref->broswerCleanUp();
			emit ref->pub->error(QZeroConf::browserFailed);
			break;
		case AVAHI_BROWSER_NEW:
			if (!ref->resolvers.contains(key))
				ref->resolvers.insert(key, avahi_service_resolver_new(ref->client, interface, protocol, name, type, domain, ref->aProtocol, AVAHI_LOOKUP_USE_MULTICAST, resolveCallback, ref));
			break;
		case AVAHI_BROWSER_REMOVE:
			if (!ref->resolvers.contains(key))
				return;
			avahi_service_resolver_free(ref->resolvers[key]);
			ref->resolvers.remove(key);

			if (!ref->pub->services.contains(key))
				return;

			zcs = ref->pub->services[key];
			ref->pub->services.remove(key);
			emit ref->pub->serviceRemoved(zcs);
			break;
		case AVAHI_BROWSER_ALL_FOR_NOW:
		case AVAHI_BROWSER_CACHE_EXHAUSTED:
			break;
		}
	}

	static void resolveCallback(
	    AVAHI_GCC_UNUSED AvahiServiceResolver *r,
	    AVAHI_GCC_UNUSED AvahiIfIndex interface,
	    AVAHI_GCC_UNUSED AvahiProtocol protocol,
	    AvahiResolverEvent event,
	    const char *name,
	    const char *type,
	    const char *domain,
	    const char *host_name,
	    const AvahiAddress *address,
	    uint16_t port,
	    AvahiStringList *txt,
	    AvahiLookupResultFlags,
	    AVAHI_GCC_UNUSED void* userdata)
	{
		bool newRecord = 0;
		QZeroConfService zcs;
		QZeroConfPrivate *ref = static_cast<QZeroConfPrivate *>(userdata);

		QString key = name + QString::number(interface);
		if (event == AVAHI_RESOLVER_FOUND) {
			if (ref->pub->services.contains(key))
				zcs = ref->pub->services[key];
			else {
				zcs = QZeroConfService(new QZeroConfServiceData);
				newRecord = 1;
				zcs->m_name = name;
				zcs->m_type = type;
				zcs->m_domain = domain;
				zcs->m_host = host_name;
				zcs->m_interfaceIndex = interface;
				zcs->m_port = port;
				while (txt)	// get txt records
				{
					QByteArray avahiText((const char *)txt->text, txt->size);
					const ssize_t pos = avahiText.indexOf('=');
					if (pos < 0)
						zcs->m_txt[avahiText] = "";
					else
						zcs->m_txt[avahiText.left(pos)] = avahiText.mid(pos + 1, -1);
					txt = txt->next;
				}
				ref->pub->services.insert(key, zcs);
			}

			char a[AVAHI_ADDRESS_STR_MAX];
			avahi_address_snprint(a, sizeof(a), address);
			QHostAddress addr(a);
			zcs->setIp(addr);

			if (newRecord)
				emit ref->pub->serviceAdded(zcs);
			else
				emit ref->pub->serviceUpdated(zcs);
		}
		else if (ref->pub->services.contains(key)) {	// delete service if exists and unable to resolve
			zcs = ref->pub->services[key];
			ref->pub->services.remove(key);
			emit ref->pub->serviceRemoved(zcs);
			// don't delete the resolver here...we need to keep it around so Avahi will keep updating....might be able to resolve the service in the future
		}
	}

	void broswerCleanUp(void)
	{
		if (!browser)
			return;
		avahi_service_browser_free(browser);
		browser = NULL;

		QMap<QString, QZeroConfService>::iterator i;
		for (i = pub->services.begin(); i != pub->services.end(); i++) {
			emit pub->serviceRemoved(i.value());
		}
		pub->services.clear();

		QMap<QString, AvahiServiceResolver *>::iterator r;
		for (r = resolvers.begin(); r != resolvers.end(); r++)
		    avahi_service_resolver_free(*r);
		resolvers.clear();
	}

	QZeroConf *pub;
	const AvahiPoll *poll;
	AvahiClient *client;
	AvahiEntryGroup *group;
	AvahiServiceBrowser *browser;
	AvahiProtocol aProtocol;
	QMap <QString, AvahiServiceResolver *> resolvers;
	AvahiStringList *txt;
};


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

QZeroConf::~QZeroConf()
{
	avahi_string_list_free(pri->txt);
	pri->broswerCleanUp();
	if (pri->client)
		avahi_client_free(pri->client);
	delete pri;
}

void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
	if (!pri->client || pri->group) {  // check client is ok (avahi daemon is running) and group is not already configured
		emit error(QZeroConf::serviceRegistrationFailed);
		return;
	}
	if (interface <= 0) {
		interface = AVAHI_IF_UNSPEC;
	}

	pri->group = avahi_entry_group_new(pri->client, QZeroConfPrivate::groupCallback, pri);

	int ret = avahi_entry_group_add_service_strlst(pri->group, interface, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UPDATE, name, type, domain, NULL, port, pri->txt);
	if (ret < 0) {
		avahi_entry_group_free(pri->group);
		pri->group = NULL;
		emit error(QZeroConf::serviceRegistrationFailed);
		return;
	}

	ret = avahi_entry_group_commit(pri->group);
	if (ret < 0) {
		pri->group = NULL;
		avahi_entry_group_free(pri->group);
		emit error(QZeroConf::serviceRegistrationFailed);
	}

	if (!pri->group)
		emit error(QZeroConf::serviceRegistrationFailed);
}

void QZeroConf::stopServicePublish(void)
{
	if (pri->group) {
		avahi_entry_group_free(pri->group);
		pri->group = NULL;
	}
}

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

// http://www.zeroconf.org/rendezvous/txtrecords.html

void QZeroConf::addServiceTxtRecord(QString nameOnly)
{
	pri->txt = avahi_string_list_add(pri->txt, nameOnly.toUtf8());
}

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

void QZeroConf::clearServiceTxtRecords()
{
	avahi_string_list_free(pri->txt);
	pri->txt = NULL;
}

void QZeroConf::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol)
{
	if (!pri->client || pri->browser) {  // check client is ok (avahi daemon is running) and browser is not already started
		emit error(QZeroConf::browserFailed);
		return;
	}

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

	pri->browser = avahi_service_browser_new(pri->client, AVAHI_IF_UNSPEC, pri->aProtocol, type.toUtf8(), NULL, AVAHI_LOOKUP_USE_MULTICAST, QZeroConfPrivate::browseCallback, pri);
	if (!pri->browser)
		emit error(QZeroConf::browserFailed);
}

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

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