/* avahi-publish.cc - publish network ports with avahi
 * Copyright 2009 Bas Wijnen <wijnen@debian.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "avahi.hh"
#include <avahi-glib/glib-malloc.h>

#include "iostring.hh"
#include "error.hh"
#include "debug.hh"

namespace shevek
{
	void avahi::publish (Glib::ustring const &protocol, int port)
	{
		if (!m_name)
			shevek_error ("cannot publish without a service name");
		if (m_ports.find (protocol) != m_ports.end ())
			shevek_error (shevek::ostring ("duplicate registration of protocol %s", protocol));
		m_ports[protocol] = port;
		create_services (m_client);
	}

	void avahi::name_change (AvahiClient *client)
	{
		char *n = avahi_alternative_service_name (m_name);
		avahi_free (m_name);
		m_name = n;
		dbg (shevek::ostring ("name collision; renamed to %s", m_name));
		create_services (client);
	}

	void avahi::group_callback (AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
	{
		avahi *self = (avahi *)userdata;
		dbg ("group state change: " << state);
		AvahiClient *client = avahi_entry_group_get_client (g);
		switch (state)
		{
			case AVAHI_ENTRY_GROUP_UNCOMMITED:
			case AVAHI_ENTRY_GROUP_REGISTERING:
				break;
			case AVAHI_ENTRY_GROUP_ESTABLISHED:
				dbg ("Established a group");
				break;
			case AVAHI_ENTRY_GROUP_COLLISION:
				self->name_change (client);
				break;
			case AVAHI_ENTRY_GROUP_FAILURE:
				if (avahi_client_errno (client) != AVAHI_ERR_DISCONNECTED)
					shevek_error (shevek::ostring ("group failure: %s", avahi_strerror (avahi_client_errno (client))));
				self->create_client ();
				break;
			default:
				shevek_error (shevek::ostring ("invalid case %d", state));
		}
	}

	void avahi::create_services (AvahiClient *client)
	{
		if (!m_name || avahi_client_get_state (client) != AVAHI_CLIENT_S_RUNNING)
			return;
		if (!m_group)
		{
			m_group = avahi_entry_group_new (client, &group_callback, this);
			if (!m_group)
				shevek_error (shevek::ostring ("failed to create group: %s", avahi_strerror (avahi_client_errno (client))));
		}
		if (m_ports.empty () || !avahi_entry_group_is_empty (m_group))
			return;
		for (std::map <Glib::ustring, int>::iterator i = m_ports.begin (); i != m_ports.end (); ++i)
		{
			Glib::ustring p = Glib::ustring (1, '_') + i->first + "._tcp";
			int ret = avahi_entry_group_add_service (m_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0, m_name, p.c_str (), NULL, NULL, i->second, NULL);
			if (ret < 0)
			{
				if (ret == AVAHI_ERR_COLLISION)
					return name_change (client);
				else
					shevek_error (shevek::ostring ("avahi error: %s", avahi_strerror (avahi_client_errno (client))));
			}
		}
		int ret = avahi_entry_group_commit (m_group);
		if (ret < 0)
			shevek_error (shevek::ostring ("error committing group: %s", avahi_strerror (avahi_client_errno (client))));
	}

	void avahi::callback (AvahiClient *client, AvahiClientState state, void *userdata)
	{
		avahi *self = (avahi *)userdata;
		dbg ("state change: " << state);
		switch (state)
		{
			case AVAHI_CLIENT_S_RUNNING:
				self->create_services (client);
				break;
			case AVAHI_CLIENT_S_COLLISION:
			case AVAHI_CLIENT_S_REGISTERING:
				if (self->m_group)
					avahi_entry_group_reset (self->m_group);
				break;
			case AVAHI_CLIENT_FAILURE:
				shevek_error ("client reports failure");
				break;
			case AVAHI_CLIENT_CONNECTING:
				break;
			default:
				shevek_warning (shevek::ostring ("unknown state %d", state));
				break;
		}
	}

	void avahi::create_client ()
	{
		if (m_client)
			avahi_client_free (m_client);
		int error;
		m_client = avahi_client_new (m_poll_api, m_allow_restart ? AVAHI_CLIENT_NO_FAIL :(AvahiClientFlags)0, &callback, this, &error);
		if (!m_client)
			shevek_error (shevek::ostring ("error creating client: %s", avahi_strerror (avahi_client_errno (m_client))));
	}

	avahi::avahi (Glib::ustring const &name, bool allow_restart, bool blocking_poller)
	{
		m_name = name.empty () ? NULL : avahi_strdup (name.c_str ());
		m_allow_restart = allow_restart;
		m_client = NULL;
		m_group = NULL;
		if (blocking_poller)
		{
			m_poller = avahi_simple_poll_new ();
			m_poll_api = avahi_simple_poll_get (m_poller);
		}
		else
		{
			m_poller = NULL;
			m_glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
			m_poll_api = avahi_glib_poll_get (m_glib_poll);
		}
		create_client ();
	}

	avahi::~avahi ()
	{
		avahi_client_free (m_client);
		if (m_poller)
			avahi_simple_poll_free (m_poller);
		else
			avahi_glib_poll_free (m_glib_poll);
		avahi_free (m_name);
	}

	static int _setup_malloc ()
	{
		avahi_set_allocator (avahi_glib_allocator ());
		return 0;
	}

	static G_GNUC_UNUSED int _dummy = _setup_malloc ();

	avahi::browser::browser (Glib::RefPtr <avahi> parent, Glib::ustring const &protocol)
	{
		m_parent = parent;
		Glib::ustring p = Glib::ustring (1, '_') + protocol + "._tcp";
		m_sb = avahi_service_browser_new(m_parent->m_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, p.c_str (), NULL, (AvahiLookupFlags)0, &browse_callback, this);
		if (!m_sb)
			shevek_error (shevek::ostring ("unable to open browser: %s", avahi_strerror (avahi_client_errno (m_parent->m_client))));
	}

	avahi::browser::~browser ()
	{
		avahi_service_browser_free (m_sb);
	}

	void avahi::browser::resolve_callback (AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, char const *name, char const *type, char const *domain, char const *host_name, AvahiAddress const *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void * userdata)
	{
		dbg ("resolved " << name);
		browser *self = (browser *)userdata;
		if (event == AVAHI_RESOLVER_FAILURE)
			shevek_error (shevek::ostring ("Error resolving service: %s", avahi_strerror (avahi_client_errno (self->m_parent->m_client))));
		if (event != AVAHI_RESOLVER_FOUND)
			shevek_error ("Invalid case");
		char a[AVAHI_ADDRESS_STR_MAX];
		avahi_address_snprint (a, sizeof(a), address);
		list::iterator i = self->m_list.find (name);
		if (i == self->m_list.end ())
			i = self->m_list.insert (std::make_pair (name, owner (host_name, port))).first;
		details d (interface, protocol, a, flags);
		if (i->second.details.find (d) != i->second.details.end ())
			shevek_error (shevek::ostring ("registering already registered service %s with identical interface and protocol", name));
		i->second.details.insert (d);
		self->m_changed (name);
		avahi_service_resolver_free (r);
	}

	void avahi::browser::browse_callback (AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, char const *name, char const *type, char const *domain, AvahiLookupResultFlags flags, void *userdata)
	{
		browser *self = (browser *)userdata;
		switch (event)
		{
			case AVAHI_BROWSER_NEW:
			{
				dbg ("new " << name);
				if (!self->m_filter.empty () && self->m_filter != name)
					break;
				AvahiServiceResolver *resolver = avahi_service_resolver_new (self->m_parent->m_client, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, (AvahiLookupFlags)0, &resolve_callback, self);
				if (!resolver)
					shevek_error (shevek::ostring ("Failed to resolve service '%s': %s\n", name, avahi_strerror (avahi_client_errno (self->m_parent->m_client))));
				break;
			}
			case AVAHI_BROWSER_REMOVE:
			{
				dbg ("remove " << name);
				list::iterator i = self->m_list.find (name);
				if (i == self->m_list.end ())
				{
					shevek_warning (shevek::ostring ("remove event for %s, which is not registered", name));
					return;
				}
				details_list::iterator j = i->second.details.find (details (interface, protocol));
				if (j == i->second.details.end ())
				{
					shevek_warning (shevek::ostring ("remove event for %s, which is not registered", name));
					return;
				}
				i->second.details.erase (j);
				if (i->second.details.empty ())
					self->m_list.erase (i);
				self->m_changed (name);
				break;
			}
			case AVAHI_BROWSER_CACHE_EXHAUSTED:
				dbg ("cache exhausted");
				break;
			case AVAHI_BROWSER_ALL_FOR_NOW:
				dbg ("all for now");
				if (self->m_parent->m_poller)
					avahi_simple_poll_quit (self->m_parent->m_poller);
				break;
			case AVAHI_BROWSER_FAILURE :
				shevek_error (shevek::ostring ("Error: %s", avahi_strerror (avahi_client_errno (self->m_parent->m_client))));
			default:
				shevek_error ("Error: invalid case");
		}
	}

	avahi::browser::list avahi::browser::get_list_block (Glib::ustring const &protocol, Glib::ustring const &name)
	{
		Glib::RefPtr <avahi> parent (new avahi (name, true, true));
		Glib::RefPtr <browser> self (new browser (parent, protocol));
		self->m_filter = name;
		avahi_simple_poll_loop (parent->m_poller);
		return self->get_list ();
	}
}
