/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/* gnome-vfs-dns-sd.c - DNS-SD functions

   Copyright (C) 2004 Red Hat, Inc

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

   The Gnome Library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Gnome Library; see the file COPYING.LIB.  If not,
   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.

   Author: Alexander Larsson <alexl@redhat.com>
*/

#include <config.h>

#if HAVE_UNISTD_H
# include <unistd.h>
#endif /* #if HAVE_UNISTD_H */
#include <sys/time.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <string.h>
#include "gnome-vfs-dns-sd.h"
#include <gconf/gconf-client.h>
#include <unistd.h>

#ifdef HAVE_AVAHI
#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-common/error.h>
#include <avahi-common/simple-watch.h>
#include <avahi-common/timeval.h>
#include <avahi-glib/glib-watch.h>
#endif 

#ifdef HAVE_HOWL
/* Need to work around howl exporting its config file... */
#undef PACKAGE
#undef VERSION
#include <howl.h>
#endif

#define PATH_GCONF_GNOME_VFS_DNS_SD "/system/dns_sd"
#define PATH_GCONF_GNOME_VFS_DNS_SD_EXTRA_DOMAINS "/system/dns_sd/extra_domains"

#define DNS_REPLY_SIZE (64*1024)


/* Some systems don't those namespaced constants defined, see 
 * http://bugzilla.gnome.org/show_bug.cgi?id=162289
 * I'm assuming that if 1 constant isn't defined, then all the others
 * are missing too, please file a bug if that's not the case on your system
 */
#ifndef NS_MAXDNAME
#define NS_MAXDNAME MAXDNAME
#define NS_HFIXEDSZ HFIXEDSZ
#define ns_c_in     C_IN
#define ns_t_any    T_ANY
#define ns_t_srv    T_SRV
#define ns_t_txt    T_TXT
#define ns_t_ptr    T_PTR
#endif /* NS_MAXDNAME */

/* Unicast DNS browsing: */

typedef struct {
	guint16 id;
	guint16 flags;
	guint16 qdcount;
	guint16 ancount;
	guint16 nscount;
	guint16 arcount;
} dns_message_header;

typedef struct {
	char name[NS_MAXDNAME];
	guint16 type;
	guint16 class;
	guint32 ttl;
	guint16 rdlength;
} dns_message_rr;

/* Normally, this would be autogenerated by glib-mkenums, but in this case it
   has no way to guess correctly. */
GType
gnome_vfs_dns_sd_service_status_get_type (void)
{
  static GType etype = 0;
  if (etype == 0) {
    static const GEnumValue values[] = {
      { GNOME_VFS_DNS_SD_SERVICE_ADDED, "GNOME_VFS_DNS_SD_SERVICE_ADDED", "added" },
      { GNOME_VFS_DNS_SD_SERVICE_REMOVED, "GNOME_VFS_DNS_SD_SERVICE_REMOVED", "removed" },
      { 0, NULL, NULL }
    };
    etype = g_enum_register_static ("GnomeVFSDNSSDServiceStatus", values);
  }
  return etype;
}

static GHashTable *
decode_txt_record (char *raw_txt,
		   int raw_txt_len)
{
	GHashTable *hash;
	int i;
	int len;
	char *key, *value, *end;
	char *key_dup, *value_dup;

	if (raw_txt == NULL)
		return NULL;
	
	hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

	i = 0;
	while (i < raw_txt_len) {
		len = raw_txt[i++];
		
		if (i + len > raw_txt_len) {
			break;
		}
		
		if (len == 0) {
			continue;
		}
		
		key = &raw_txt[i];
		end = &raw_txt[i + len];
		i += len;

		if (*key == '=') {
			/* 6.4 - silently ignore keys starting with = */
			continue;
		}
		
		value = memchr (key, '=', len);
		if (value) {
			key_dup = g_strndup (key, value - key);
			value++; /* Skip '=' */
			value_dup = g_strndup (value, end - value);
		} else {
			key_dup = g_strndup (key, len);
			value_dup = NULL;
		}
		if (!g_hash_table_lookup_extended (hash,
						   key_dup,
						   NULL, NULL)) {
			g_hash_table_insert (hash,
					     key_dup,
					     value_dup);
		} else {
			g_free (key_dup);
			g_free (value_dup);
		}
	}

	return hash;
}


static guint16
decode_16 (unsigned char *p)
{
	return p[0] << 8 | p[1];
}

static guint32
decode_32 (unsigned char *p)
{
	return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
}

static void
split_service_instance (char *name,
			char *service,
			char *type,
			char *domain)
{
	int i, n_dots;

	i = 0;
	while (*name != 0) {
		if (*name == '.') {
			name++;
			break;
		}
		if (*name == '\\') {
			name++;
			if (g_ascii_isdigit (*name)) {
				if (g_ascii_isdigit (name[1]) &&
				    g_ascii_isdigit (name[2])) {
					service[i++] =
						g_ascii_digit_value (name[0]) * 100 +
						g_ascii_digit_value (name[1]) * 10 +
						g_ascii_digit_value (name[2]);
					name += 3;
				}
			} else if (*name != 0) {
				service[i++] = *name++;
			}
		} else {
			service[i++] = *name++;
		}
	}
	service[i] = 0;
	
	i = 0;
	n_dots = 0;
	while (*name != 0) {
		if (*name == '.') {
			n_dots++;
		}
		if (n_dots == 2) {
			name++;
			break;
		}
		type[i++] = *name++;
	}
	type[i] = 0;
	
	i = 0;
	while (*name != 0) {
		domain[i++] = *name++;
	}
	domain[i] = 0;
}

static gboolean
is_valid_dns_sd_type (const char *type)
{
	return type[0] == '_' &&
		(g_str_has_suffix (type, "._tcp") ||
		 g_str_has_suffix (type, "._udp"));
}

static int
parse_header (unsigned char *reply, int reply_len,
	      unsigned char *p,
	      dns_message_header *header)
{
	if (reply_len < NS_HFIXEDSZ)
		return -1;

	header->id = decode_16 (p); p += 2;
	header->flags = decode_16 (p); p += 2;
	header->qdcount = decode_16 (p); p += 2;
	header->ancount = decode_16 (p); p += 2;
	header->nscount = decode_16 (p); p += 2;
	header->arcount = decode_16 (p); p += 2;

	return NS_HFIXEDSZ;
}


static int
parse_qs (unsigned char *reply, int reply_len,
	  unsigned char *p,
	  char *name, int name_size,
	  int *qtype, int *qclass)
{
	int len;
	unsigned char *start, *end;
	
	start = p;
	end = reply + reply_len;
	
	if (p >= end)
		return -1;
	len = dn_expand (reply, reply + reply_len, p, name, name_size);
	if (len < 0)
		return -1;
	
	p += len;
	
	if (p + 4 > end)
		return -1;
	
	*qtype = decode_16 (p); p += 2;
	*qclass = decode_16 (p); p += 2;
	
	return p - start;
}

static int
parse_rr (unsigned char *reply, int reply_len,
	  unsigned char *p,
	  dns_message_rr *rr)
{
	int len;
	unsigned char *start, *end;
	
	start = p;
	end = reply + reply_len;
	
	if (p >= end)
		return -1;
	len = dn_expand (reply, reply + reply_len, p, rr->name, sizeof (rr->name));
	if (len < 0)
		return -1;
	p += len;
	
	if (p + 10 > end)
		return -1;
	
	rr->type = decode_16 (p); p += 2;
	rr->class = decode_16 (p); p += 2;
	rr->ttl = decode_32 (p); p += 4;
	rr->rdlength = decode_16 (p); p += 2;
	
	if (p + rr->rdlength > end)
		return -1;

	return p - start;
}

static GnomeVFSResult
unicast_list_domains_sync (const char *domain,
			   GList **domains)
{
	int reply_len, len, i;
	unsigned char reply[DNS_REPLY_SIZE];
	unsigned char *p;
	char *searchdomain;
	char name[NS_MAXDNAME];
	dns_message_header header;
	dns_message_rr rr;
	int res;
	GList *l;

	*domains = NULL;
	
	res = res_init ();
	if (res != 0) {
		return GNOME_VFS_ERROR_INTERNAL;
	}
	
	/* Use TCP to support large queries */
	_res.options |= RES_USEVC;
	searchdomain = g_strconcat ("_browse._dns-sd._udp.", domain, NULL);
	reply_len = res_search (searchdomain, ns_c_in, ns_t_ptr,
				reply, sizeof (reply));
	g_free (searchdomain);
	if (reply_len == -1) {
		return GNOME_VFS_ERROR_GENERIC;
	}
  
	/* Parse out query */
	p = reply;
	
	len = parse_header (reply, reply_len, p,
			    &header);
	if (len < 0) {
		goto error;
	}
	p += len;
	
	if ((header.flags & (1 << 15)) == 0) {
		/* Not a reply */
		goto error;
	}
	
	if (header.flags & (1 << 9)) {
		/* Truncated */
		goto error;
	}

	for (i = 0; i < header.qdcount; i++) {
		int qtype, qclass;
		
		len = parse_qs (reply, reply_len, p,
				name, sizeof(name),
				&qtype, &qclass);
		if (len < 0) 
			goto error;
		p += len;
	}

	for (i = 0; i < header.ancount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		if (rr.type == ns_t_ptr) {
			len = dn_expand (reply, reply + reply_len, p, name, sizeof(name));
			if (len < 0) 
				goto error;

			*domains = g_list_prepend (*domains, g_strdup (name));
		}
		
		p += rr.rdlength;
	}
	
	for (i = 0; i < header.nscount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
	}
	
	for (i = 0; i < header.arcount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
	}

	return GNOME_VFS_OK;
	
 error:
	for (l = *domains; l != NULL; l = l->next) {
		g_free (l->data);
	}
	g_list_free (*domains);
	*domains = NULL;
	
	return GNOME_VFS_ERROR_GENERIC;
}


static GnomeVFSResult
unicast_browse_sync (const char *domain, const char *type,
		     int *num_services,
		     GnomeVFSDNSSDService **services)
{
	int reply_len, len, i;
	unsigned char reply[DNS_REPLY_SIZE];
	unsigned char *p;
	char *searchdomain;
	char name[NS_MAXDNAME];
	dns_message_header header;
	dns_message_rr rr;
	GArray *array;
	GnomeVFSDNSSDService *service;
	int res;

	array = NULL;
	
	res = res_init ();
	if (res != 0) {
		return GNOME_VFS_ERROR_INTERNAL;
	}
	
	/* Use TCP to support large queries */
	_res.options |= RES_USEVC;
	searchdomain = g_strconcat (type, ".", domain, NULL);
	reply_len = res_search (searchdomain, ns_c_in, ns_t_ptr,
				reply, sizeof (reply));
	g_free (searchdomain);
	if (reply_len == -1) {
		return GNOME_VFS_ERROR_GENERIC;
	}
  
	/* Parse out query */
	p = reply;
	
	len = parse_header (reply, reply_len, p,
			    &header);
	if (len < 0) {
		goto error;
	}
	p += len;
	
	if ((header.flags & (1 << 15)) == 0) {
		/* Not a reply */
		goto error;
	}
	
	if (header.flags & (1 << 9)) {
		/* Truncated */
		goto error;
	}

	for (i = 0; i < header.qdcount; i++) {
		int qtype, qclass;
		
		len = parse_qs (reply, reply_len, p,
				name, sizeof(name),
				&qtype, &qclass);
		if (len < 0) 
			goto error;
		p += len;
	}

	array = g_array_new (FALSE, FALSE, sizeof (GnomeVFSDNSSDService));

	for (i = 0; i < header.ancount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		if (rr.type == ns_t_ptr) {
			GnomeVFSDNSSDService service;
			char ptr_service[NS_MAXDNAME];
			char ptr_type[NS_MAXDNAME];
			char ptr_domain[NS_MAXDNAME];
			
			len = dn_expand (reply, reply + reply_len, p, name, sizeof(name));
			if (len < 0) 
				goto error;
			
			split_service_instance (name, ptr_service, ptr_type, ptr_domain);

			if (is_valid_dns_sd_type (ptr_type)) {
				service.name = g_strdup (ptr_service);
				service.type = g_strdup (ptr_type);
				service.domain = g_strdup (ptr_domain);

				g_array_append_val(array, service);
			}
		}
		
		p += rr.rdlength;
	}
	
	for (i = 0; i < header.nscount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
	}
	
	for (i = 0; i < header.arcount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
	}

	*num_services = array->len;
	*services = (GnomeVFSDNSSDService *)g_array_free (array, FALSE);
	
	return GNOME_VFS_OK;
	
 error:
	for (i = 0; i < array->len; i++) {
		service = &g_array_index (array, GnomeVFSDNSSDService, i);
		g_free (service->name);
		g_free (service->type);
		g_free (service->domain);
	}
	g_array_free (array, TRUE);
	
	return GNOME_VFS_ERROR_GENERIC;
}

static char *
service_to_dns_name (const char *name, const char *type, const char *domain)
{
	GString *string;
	const char *p;

	string = g_string_new (NULL);

	p = name;

	while (*p) {
		if (*p == '\\') 
			g_string_append (string, "\\\\");
		else if (*p == '.') 
			g_string_append (string, "\\.");
		else
			g_string_append_c (string, *p);
		p++;
	}
	g_string_append_c (string, '.');
	g_string_append (string, type);
	g_string_append_c (string, '.');
	g_string_append (string, domain);

	return g_string_free (string, FALSE);
}

static GnomeVFSResult
unicast_resolve_sync (const char *name,
		      const char *type,
		      const char *domain,
		      char **host,
		      int *port_out,
		      int *text_raw_len_out,
		      char **text_raw_out)
{
	int reply_len, len, i;
	unsigned char reply[DNS_REPLY_SIZE];
	unsigned char *p;
	char dnsname[NS_MAXDNAME];
	dns_message_header header;
	dns_message_rr rr;
	char *full_name;
	int res;

	*host = NULL;
	*port_out = 0;
	*text_raw_len_out = 0;
	*text_raw_out = NULL;
	
	res = res_init ();
	if (res != 0) {
		return GNOME_VFS_ERROR_INTERNAL;
	}

	/* Use TCP to support large queries */
	_res.options |= RES_USEVC;
  
	full_name = service_to_dns_name (name, type, domain);
	reply_len = res_search (full_name, ns_c_in, ns_t_any,
				reply, sizeof (reply));
	g_free (full_name);
	if (reply_len == -1) {
		return GNOME_VFS_ERROR_GENERIC;
	}
  
	/* Parse out query */
	p = reply;
	
	len = parse_header (reply, reply_len, p,
			    &header);
	if (len < 0)
		goto error;
	p += len;
	
	if ((header.flags & (1 << 15)) == 0) {
		/* Not a reply */
		goto error;
	}
	
	if (header.flags & (1 << 9)) {
		/* Truncated */
		g_warning ("dns-sd reply truncated!\n");
		goto error;
	}
	
	for (i = 0; i < header.qdcount; i++) {
		int qtype, qclass;
		
		len = parse_qs (reply, reply_len, p,
				dnsname, sizeof(dnsname),
				&qtype, &qclass);
		if (len < 0) 
			goto error;
		p += len;
	}
	
	for (i = 0; i < header.ancount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		if (rr.type == ns_t_srv) {
			unsigned char *pp;
			int priority, weight, port;
			
			pp = p;
			
			priority = decode_16 (pp); pp += 2;
			weight = decode_16 (pp); pp += 2;
			port = decode_16 (pp); pp += 2;
			
			len = dn_expand (reply, reply + reply_len, pp, dnsname, sizeof(dnsname));
			if (len < 0) 
				goto error;

			/* TODO: look at prio and weigth. For now use the first */
			if (*host == NULL) {
				*host = g_strdup (dnsname);
				*port_out = port;
			}
		}
		
		if (rr.type == ns_t_txt) {
			*text_raw_out = g_memdup (p, rr.rdlength);
			*text_raw_len_out = rr.rdlength;
		}
		
		p += rr.rdlength;
	}
	
	for (i = 0; i < header.nscount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
		
	}
	
	for (i = 0; i < header.arcount; i++) {
		len = parse_rr (reply, reply_len, p, &rr);
		if (len < 0) 
			goto error;
		p += len;
		
		p += rr.rdlength;
	}

	return GNOME_VFS_OK;
	
 error:
	g_free (*host);
	*host = NULL;
	g_free (*text_raw_out);
	*text_raw_out = NULL;
	
	return GNOME_VFS_ERROR_GENERIC;
}


/* multicast DNS functions */

#ifdef HAVE_AVAHI

static AvahiClient *global_client = NULL;
static gboolean avahi_initialized = FALSE;
static GSList *browse_handles = NULL;

static AvahiClient *get_global_avahi_client (void);
static void add_service_browser_cb (gpointer data, gpointer user_data);
static void remove_service_browser_cb (gpointer data, gpointer user_data);

/* Callback for state changes on the Client */
static void
avahi_client_callback (AvahiClient *client, AvahiClientState state, void *userdata)
{
	if (state == AVAHI_CLIENT_FAILURE) {
		if (avahi_client_errno (client) == AVAHI_ERR_DISCONNECTED) {
			/* Remove the service browsers from the handles */
			g_slist_foreach (browse_handles, remove_service_browser_cb, NULL);
			
			/* Destroy old client */
			avahi_client_free (client);
			global_client = NULL;
			avahi_initialized = FALSE;

			/* Reconnect */
			get_global_avahi_client ();
		}
	} else if (state == AVAHI_CLIENT_S_RUNNING) {
		/* Start browsing again */
		g_slist_foreach (browse_handles, add_service_browser_cb, NULL);
	}
}

static AvahiClient *
get_global_avahi_client (void) {
	static AvahiGLibPoll *glib_poll = NULL;
	int error;

	if (!avahi_initialized) {
		if (glib_poll == NULL) {
			glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
		}

		/* Create a new AvahiClient instance */
		global_client = avahi_client_new (avahi_glib_poll_get (glib_poll),
						  AVAHI_CLIENT_NO_FAIL,
						  avahi_client_callback,
						  glib_poll,
						  &error);

		if (global_client == NULL) {    
			/* Print out the error string */
			g_warning ("Error initializing Avahi: %s", avahi_strerror (error));
			avahi_glib_poll_free (glib_poll);
			glib_poll = NULL;
			return NULL;
		}
		avahi_initialized = TRUE;
	}

	return global_client;
}
#endif

#ifdef HAVE_HOWL

static gboolean
howl_input (GIOChannel  *io_channel,
	    GIOCondition cond,
	    gpointer     callback_data)
{
	sw_discovery session;
	sw_salt salt;
	session = callback_data;

	if (sw_discovery_salt (session, &salt) == SW_OKAY) {
		sw_salt_lock (salt);
		sw_discovery_read_socket (session);
		sw_salt_unlock (salt);
	}
	return TRUE;
}


static void
set_up_howl_session (sw_discovery session)
{
	int fd;
	GIOChannel *channel;

	fd = sw_discovery_socket (session);

	channel = g_io_channel_unix_new (fd);
	g_io_add_watch (channel,
			G_IO_IN,
			howl_input, session);
	g_io_channel_unref (channel);
}

static sw_discovery
get_global_howl_session (void) {
	static sw_discovery global_session = NULL;
	static gboolean initialized = FALSE;

	if (!initialized) {
		if (sw_discovery_init (&global_session) != SW_OKAY) {
			g_warning ("get_global_howl_session - howl init failed\n");
			return NULL;
		}
		set_up_howl_session (global_session);
		initialized = TRUE;
	}

	return global_session;
}

#endif /* HAVE_HOWL */

struct GnomeVFSDNSSDBrowseHandle {
	char *domain;
	char *type;
	GnomeVFSDNSSDBrowseCallback callback;
	gpointer callback_data;
	GDestroyNotify callback_data_destroy_func;

	gboolean is_local;
	gboolean cancelled;

	/* multicast: */

#ifdef HAVE_AVAHI
	AvahiServiceBrowser *avahi_sb;
#endif

#ifdef HAVE_HOWL
	sw_discovery_oid howl_id;
#endif
	
	/* unicast data: */
	int n_services;
	GnomeVFSDNSSDService *services;
	GnomeVFSResult res;
	gboolean finished;
};


static void
free_browse_handle (GnomeVFSDNSSDBrowseHandle *handle)
{
	int i;
	
	g_free (handle->domain);
	g_free (handle->type);
	
	for (i = 0; i < handle->n_services; i++) {
		g_free (handle->services[i].name);
		g_free (handle->services[i].type);
		g_free (handle->services[i].domain);
	}
	
	g_free (handle->services);

	if (handle->callback_data_destroy_func != NULL)
		handle->callback_data_destroy_func (handle->callback_data);
	
	g_free (handle);
}

static gboolean
unicast_browse_idle (gpointer data)
{
	GnomeVFSDNSSDBrowseHandle *handle;
	int i;
	
	handle = data;
	
	if (!handle->cancelled &&
	    handle->res == GNOME_VFS_OK) {
		for (i = 0; i < handle->n_services; i++) {
			handle->callback (data,
					  GNOME_VFS_DNS_SD_SERVICE_ADDED,
					  &handle->services[i],
					  handle->callback_data);
		}
	}

	handle->finished = TRUE;
	
	if (handle->cancelled)
		free_browse_handle (handle);
	
	return FALSE;
}


static gpointer
unicast_browse_thread (gpointer data)
{
	GnomeVFSDNSSDBrowseHandle *handle;
	handle = data;
	
	handle->res = unicast_browse_sync (handle->domain,
					   handle->type,
					   &handle->n_services,
					   &handle->services);
	g_idle_add (unicast_browse_idle,
		    handle);
	return NULL;
}

#ifdef HAVE_AVAHI

static void 
avahi_browse_callback (AvahiServiceBrowser *b,
		       AvahiIfIndex interface,
		       AvahiProtocol protocol,
		       AvahiBrowserEvent event,
		       const char *name,
		       const char *type,
		       const char *domain,
		       AvahiLookupResultFlags flags,
		       void *userdata)
{
	GnomeVFSDNSSDBrowseHandle *handle;
	GnomeVFSDNSSDService service;
	handle = userdata;
    
	service.name = (char *)name;
	service.type = (char *)type;
	service.domain = (char *)domain;
	
	if (event == AVAHI_BROWSER_FAILURE ||
	    event == AVAHI_BROWSER_ALL_FOR_NOW ||
	    event == AVAHI_BROWSER_CACHE_EXHAUSTED) {
		return;
	}
	
	if (!handle->cancelled) {
		handle->callback (handle,
				  (event == AVAHI_BROWSER_NEW) ? GNOME_VFS_DNS_SD_SERVICE_ADDED : GNOME_VFS_DNS_SD_SERVICE_REMOVED,
				  &service,
				  handle->callback_data);
	}
}

static void
remove_service_browser_cb (gpointer data, gpointer user_data)
{
	GnomeVFSDNSSDBrowseHandle *handle = data;
	handle->avahi_sb = NULL;
}

static void
add_service_browser_cb (gpointer data, gpointer user_data)
{
	GnomeVFSDNSSDBrowseHandle *handle = data;
	AvahiServiceBrowser *sb;
	AvahiClient *client;
    
	client = get_global_avahi_client ();
	if (!client)
		return;
	
	sb = avahi_service_browser_new (client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, handle->type, NULL, 
					AVAHI_LOOKUP_USE_MULTICAST, avahi_browse_callback, handle);
	if (sb != NULL) {
		handle->avahi_sb = sb;
		return;
	}

	g_warning ("Failed to create service browser: %s\n", avahi_strerror( avahi_client_errno (client)));
}

#endif

#ifdef HAVE_HOWL

struct howl_browse_idle_data {
	GnomeVFSDNSSDBrowseHandle *handle;
	GnomeVFSDNSSDServiceStatus status;
	GnomeVFSDNSSDService service;
	
};


static gboolean
howl_browse_idle (gpointer data)
{
	struct howl_browse_idle_data *idle_data;
	GnomeVFSDNSSDBrowseHandle *handle;

	idle_data = data;
	handle = idle_data->handle;

	if (handle->cancelled)
		return FALSE;

	handle->callback (handle,
			  idle_data->status,
			  &idle_data->service,
			  handle->callback_data);
	
	return FALSE;
}

static void
browse_idle_data_free (struct howl_browse_idle_data *idle_data)
{
	g_free (idle_data->service.name);
	g_free (idle_data->service.type);
	g_free (idle_data->service.domain);
	g_free (idle_data);
}

static gboolean
free_browse_handle_idle (gpointer data)
{
	free_browse_handle (data);
	return FALSE;
}

static sw_result
howl_browse_reply (sw_discovery                 discovery,
		   sw_discovery_oid             oid,
		   sw_discovery_browse_status   status,
		   sw_uint32			interface_index,
		   sw_const_string              name,
		   sw_const_string              type,
		   sw_const_string              domain,
		   sw_opaque                    extra)
{
	GnomeVFSDNSSDBrowseHandle *handle;
	struct howl_browse_idle_data *idle_data;
	int len;

	handle = extra;
	
	if (status == SW_DISCOVERY_BROWSE_RELEASE) {
		/* free in an idle to make sure the other idles are done,
		   and to give sane environment for destroy callback */
		g_idle_add (free_browse_handle_idle, handle);
		return SW_OKAY;
	}
	
	if (handle->cancelled)
		return SW_OKAY;

	idle_data = g_new (struct howl_browse_idle_data, 1);
	idle_data->handle = handle;
	
	if (status == SW_DISCOVERY_BROWSE_ADD_SERVICE) {
		idle_data->status = GNOME_VFS_DNS_SD_SERVICE_ADDED;
	} else if (status == SW_DISCOVERY_BROWSE_REMOVE_SERVICE) {
		idle_data->status = GNOME_VFS_DNS_SD_SERVICE_REMOVED;
	} else {
		g_warning ("Unknown browse status\n");
		g_free (idle_data);
		return SW_OKAY;
	}
	
	idle_data->service.name = g_strdup (name);
	idle_data->service.type = g_strdup (type);
	idle_data->service.domain = g_strdup (domain);

	/* We don't want last dots in the domain or type */
	len = strlen (idle_data->service.type);
	if (len > 0 && idle_data->service.type[len-1] == '.')
		idle_data->service.type[len-1] = 0;
	len = strlen (idle_data->service.domain);
	if (len > 0 && idle_data->service.domain[len-1] == '.')
		idle_data->service.domain[len-1] = 0;
	
	g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
			 howl_browse_idle,
			 idle_data,
			 (GDestroyNotify) browse_idle_data_free);
	return SW_OKAY;
}

#endif /* HAVE_HOWL */

/**
 * gnome_vfs_dns_sd_browse:
 * @handle: pointer to a pointer to a #GnomeVFSDNSSDBrowseHandle object.
 * @domain: dns domain to browse, or "local" for multicast DNS.
 * @type: type of service to browse for.
 * @callback: function to be called when service is discovered.
 * @callback_data: data to pass to @callback.
 * @callback_data_destroy_func: optional destructor function for @callback_data.
 *
 * Browses @domain for service of the type @type, calling @callback whenever
 * a new one is found or removed.
 *
 * The domain to use can be "local" for multicast dns on the local network
 * (known as mDNS), or it can be the domain of the current host. You can also
 * use gnome_vfs_dns_sd_list_browse_domains_sync() to get a list of domains
 * that are interested in a particular domain.
 *
 * The type is a string of the form "_type._tcp" or "_type._udp", where type
 * is a service type registered at http://www.dns-sd.org/ServiceTypes.html.
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult
gnome_vfs_dns_sd_browse (GnomeVFSDNSSDBrowseHandle **handle_out,
			 const char *domain,
			 const char *type,
			 GnomeVFSDNSSDBrowseCallback callback,
			 gpointer callback_data,
			 GDestroyNotify callback_data_destroy_func)
{
	GnomeVFSDNSSDBrowseHandle *handle;

	*handle_out = NULL;
	
	handle = g_new0 (GnomeVFSDNSSDBrowseHandle, 1);
	handle->domain = g_strdup (domain);
	handle->type = g_strdup (type);
	handle->callback = callback;
	handle->callback_data = callback_data;
	handle->callback_data_destroy_func = callback_data_destroy_func;
	
	if (strcmp (domain, "local") == 0) {
#ifdef HAVE_AVAHI
		AvahiClient *client;
		AvahiServiceBrowser *sb;

		handle->is_local = TRUE;
		client = get_global_avahi_client ();
		if (client) {
			sb = avahi_service_browser_new (client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, type, NULL, 
							AVAHI_LOOKUP_USE_MULTICAST,
							avahi_browse_callback, handle);
			if (sb != NULL) {
				handle->avahi_sb = sb;
				*handle_out = handle;
				browse_handles = g_slist_append (browse_handles, handle);
				return GNOME_VFS_OK;
			}
			g_warning ("Failed to create service browser: %s\n", avahi_strerror( avahi_client_errno (client)));
		}
		return GNOME_VFS_ERROR_GENERIC;
#elif defined (HAVE_HOWL)
		sw_result res;
		sw_discovery session;
		
		handle->is_local = TRUE;

		session = get_global_howl_session ();
		if (session) {
			res = sw_discovery_browse (session,
						   0, 
						   type, domain,
						   howl_browse_reply,
						   handle,
						   &handle->howl_id);
			
			if (res == SW_OKAY) {
				*handle_out = handle;
				return GNOME_VFS_OK;
			}
		}
		return GNOME_VFS_ERROR_GENERIC;
#else
		free_browse_handle (handle);
		return GNOME_VFS_ERROR_NOT_SUPPORTED;
#endif
	} else {
		handle->is_local = FALSE;
		if (g_thread_create (unicast_browse_thread, handle,
				     FALSE, NULL) == NULL) {
			g_free (handle->domain);
			g_free (handle->type);
			g_free (handle);
			return GNOME_VFS_ERROR_INTERNAL;
		}
		*handle_out = handle;
		return GNOME_VFS_OK;
	}
}

/**
 * gnome_vfs_dns_sd_stop_browse:
 * @handle: handle of the browse operation to be stopped.
 *
 * Stops browsing a domain started with gnome_vfs_dns_sd_browse().
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult
gnome_vfs_dns_sd_stop_browse (GnomeVFSDNSSDBrowseHandle *handle)
{
	if (handle->is_local) {
#ifdef HAVE_AVAHI
		handle->cancelled = TRUE;

        if (handle->avahi_sb)
		avahi_service_browser_free (handle->avahi_sb);
		
	browse_handles = g_slist_remove (browse_handles, handle);
	free_browse_handle (handle);
#endif
#ifdef HAVE_HOWL
		handle->cancelled = TRUE;
		sw_discovery_cancel (get_global_howl_session (), handle->howl_id);
#endif
		return GNOME_VFS_OK;
	} else {
		if (handle->finished)
			free_browse_handle (handle);
		else
			handle->cancelled = TRUE;
		return GNOME_VFS_OK;
	}
}

struct GnomeVFSDNSSDResolveHandle {
	char *name;
	char *domain;
	char *type;
	GnomeVFSDNSSDResolveCallback callback;
	gpointer callback_data;
	GDestroyNotify callback_data_destroy_func;

	gboolean is_local;

	char *host;
	int port;
	char *text;
	int text_len;
	
	/* multicast: */
#ifdef HAVE_AVAHI
	AvahiServiceResolver *avahi_sr;
#endif

#ifdef HAVE_HOWL
	sw_discovery_oid howl_id;
	guint timeout_tag;
#endif
	
	/* unicast data: */
	gboolean cancelled;
	GnomeVFSResult res;
	guint idle_tag;
};


static void
free_resolve_handle (GnomeVFSDNSSDResolveHandle *handle)
{
	g_free (handle->name);
	g_free (handle->domain);
	g_free (handle->type);
	
	g_free (handle->host);
	g_free (handle->text);

	if (handle->callback_data_destroy_func != NULL)
		handle->callback_data_destroy_func (handle->callback_data);
	
	g_free (handle);
}

static gboolean
unicast_resolve_idle (gpointer data)
{
	GnomeVFSDNSSDResolveHandle *handle;
	GnomeVFSDNSSDService service;
	GHashTable *hash;

	handle = data;
	
	if (!handle->cancelled) {
		service.name = handle->name;
		service.type = handle->type;
		service.domain = handle->domain;

		hash = decode_txt_record (handle->text,
					  handle->text_len);
		
		handle->callback (data,
				  handle->res,
				  &service,
				  handle->host,
				  handle->port,
				  hash,
				  handle->text_len,
				  handle->text,
				  handle->callback_data);

		if (hash)
			g_hash_table_destroy (hash);
	}

	free_resolve_handle (handle);
	
	return FALSE;
}

static gpointer
unicast_resolve_thread (gpointer data)
{
	GnomeVFSDNSSDResolveHandle *handle;
	handle = data;

	handle->res = unicast_resolve_sync (handle->name,
					    handle->type,
					    handle->domain,
					    &handle->host, &handle->port,
					    &handle->text_len, &handle->text);
	g_idle_add (unicast_resolve_idle,
		    handle);
	
	return NULL;
}

#ifdef HAVE_AVAHI
struct sync_resolve_data {
	AvahiSimplePoll *poll;
	AvahiIfIndex interface;
	AvahiProtocol protocol;
	gboolean got_link_local_ipv6;
	gboolean got_data;
	char *host;
	int port;
	char *text;
	int text_len;
};


static void
avahi_resolve_sync_client_callback (AvahiClient *c, AvahiClientState state, void *user_data)
{
	struct sync_resolve_data *data;

	data = user_data;
	if (state == AVAHI_CLIENT_FAILURE) {
		avahi_simple_poll_quit (data->poll);
	}
}

static void
avahi_resolve_host_name_sync_callback (AvahiHostNameResolver *r,
                                       AvahiIfIndex interface G_GNUC_UNUSED,
                                       AvahiProtocol protocol G_GNUC_UNUSED,
                                       AvahiResolverEvent event,
                                       const char *name G_GNUC_UNUSED,
                                       const AvahiAddress *address,
                                       AvahiLookupResultFlags flags G_GNUC_UNUSED, 
                                       void *user_data)
{
	struct sync_resolve_data *data;
        char a[128];

        data = user_data;
        
	if (event == AVAHI_RESOLVER_FOUND) {
		if (address->proto == AVAHI_PROTO_INET6 &&
		    address->data.ipv6.address[0] == 0xfe &&
		    address->data.ipv6.address[1] == 0x80) {
			data->got_link_local_ipv6 = TRUE;
			goto out;
		}
		data->got_data = TRUE;
		avahi_address_snprint (a, sizeof(a), address);
		data->host = g_strdup (a);
	}

 out:
	avahi_host_name_resolver_free (r);
        avahi_simple_poll_quit (data->poll);
}

static GnomeVFSResult
avahi_resolve_host_name (AvahiIfIndex interface,
                         AvahiProtocol protocol,
                         const char *host_name,
                         char **address)
{
	GnomeVFSResult result = GNOME_VFS_ERROR_GENERIC;
        struct sync_resolve_data resolve_data;
        AvahiHostNameResolver *hr = NULL;
        AvahiClient *client = NULL;
        int error = 0;

 retry:
	
	resolve_data.got_data = FALSE;
	resolve_data.got_link_local_ipv6 = FALSE;
        resolve_data.poll = avahi_simple_poll_new ();

        if (resolve_data.poll == NULL) {
                g_warning ("Failed to create simple poll object");
                goto cleanup;
        }

        client = avahi_client_new (avahi_simple_poll_get (resolve_data.poll), 0, 
				   avahi_resolve_sync_client_callback, &resolve_data, &error);
		
	/* Check wether creating the client object succeeded */
	if (client == NULL) {
		g_warning ("Failed to create client: %s\n", avahi_strerror (error));
		goto cleanup;
	}

        hr = avahi_host_name_resolver_new (client, interface, protocol, 
                                           host_name, AVAHI_PROTO_UNSPEC, 0,
                                           avahi_resolve_host_name_sync_callback, 
                                           &resolve_data);

        if (hr == NULL) {
		g_warning ("Failed to resolve host name '%s': %s\n", host_name, avahi_strerror (avahi_client_errno (client)));
                goto cleanup;
	}

        for (;;)
                if (avahi_simple_poll_iterate (resolve_data.poll, -1) != 0)
                        break;
        
	if (resolve_data.got_link_local_ipv6) {
		/* We ignore non-routable ipv6 link-local addresses here, because our
		   api doesn't give the iface, so they are useless. Prefer ipv4 address instead */
		protocol = AVAHI_PROTO_INET;

                avahi_client_free (client);
                avahi_simple_poll_free (resolve_data.poll);
		client = NULL;
		resolve_data.poll = NULL;
		
		goto retry;
	}

	if (resolve_data.got_data) {
		result = GNOME_VFS_OK;
                *address = resolve_data.host;
	} else {
		result = GNOME_VFS_ERROR_HOST_NOT_FOUND;
	}

cleanup:
        if (client)
                avahi_client_free (client);
        if (resolve_data.poll)
                avahi_simple_poll_free (resolve_data.poll);

        return result;
}

static void
avahi_resolve_async_callback (AvahiServiceResolver *r,
			      AvahiIfIndex interface,
			      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 flags,
			      void *user_data)
{
	GnomeVFSDNSSDResolveHandle *handle;
	GnomeVFSDNSSDService service;
	GHashTable *hash;
	size_t text_len;
	char *resolved = NULL;
	const char *host;
	char *text;
	char a[128];

	handle = user_data;
	if (event == AVAHI_RESOLVER_FOUND) {
		text_len = avahi_string_list_serialize (txt, NULL, 0);
		text = g_malloc (text_len);
		text_len = avahi_string_list_serialize (txt, text, text_len);

		hash = decode_txt_record (text, text_len);

		service.name = (char *)name;
		service.type = (char *)type;
		service.domain = (char *)domain;


		if (address) {
			avahi_address_snprint (a, sizeof(a), address);
			host = a;
		} else {
			g_assert (NULL != host_name);

			if (g_str_has_suffix (host_name, ".local") &&
			    GNOME_VFS_OK == avahi_resolve_host_name (interface, protocol, host_name, &resolved)) {
				host = resolved;
			} else {
				host = host_name;
			}
		}
		
		handle->callback (handle,
				  GNOME_VFS_OK,
				  &service,
				  host,
				  port,
				  hash,
				  handle->text_len,
				  handle->text,
				  handle->callback_data);

		g_free (resolved);
		
		if (hash) {
			g_hash_table_destroy (hash);
		}

		g_free (text);
	} else if (event == AVAHI_RESOLVER_FAILURE) {
		handle->callback (handle,
				  GNOME_VFS_ERROR_HOST_NOT_FOUND,
				  NULL,
				  NULL, 0,
				  NULL, 0, NULL,
				  handle->callback_data);
	}
	
	avahi_service_resolver_free (r);
	free_resolve_handle (handle);
}

#endif


#ifdef HAVE_HOWL
static gboolean
howl_resolve_idle (gpointer data)
{
	GnomeVFSDNSSDResolveHandle *handle;
	GnomeVFSDNSSDService service;
	GHashTable *hash;

	handle = data;

	hash = decode_txt_record (handle->text,
				  handle->text_len);

	service.name = handle->name;
	service.type = handle->type;
	service.domain = handle->domain;

	handle->callback (handle,
			  GNOME_VFS_OK,
			  &service,
			  handle->host,
			  handle->port,
			  hash,
			  handle->text_len,
			  handle->text,
			  handle->callback_data);

	if (hash) {
		g_hash_table_destroy (hash);
	}

	free_resolve_handle (handle);
	
	return FALSE;
}



static sw_result
howl_resolve_reply (sw_discovery                   discovery,
		    sw_discovery_oid               id,
		    sw_uint32 			   interface_index,
		    sw_const_string                name,
		    sw_const_string                type,
		    sw_const_string                domain,
		    sw_ipv4_address                address,
		    sw_port                        port,
		    sw_octets                      text_record,
		    sw_ulong                       text_record_len,
		    sw_opaque                      extra)
{
	GnomeVFSDNSSDResolveHandle *handle;

	handle = extra;

	g_assert (handle->idle_tag == 0);

	handle->host = g_malloc (16);
	sw_ipv4_address_name (address, handle->host, 16);
	handle->port = port;
	handle->text = g_memdup (text_record, text_record_len);
	handle->text_len = text_record_len;

	/* We want no more replies */
	sw_discovery_cancel (get_global_howl_session (),
			     handle->howl_id);
	g_source_remove (handle->timeout_tag);
	
	handle->idle_tag = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
					    howl_resolve_idle,
					    handle,
					    (GDestroyNotify) NULL);
	return SW_OKAY;
}

static gboolean
howl_resolve_timeout (gpointer data)
{
	GnomeVFSDNSSDResolveHandle *handle;
	
	handle = data;

	handle->callback (handle,
			  GNOME_VFS_ERROR_HOST_NOT_FOUND,
			  NULL,
			  NULL, 0,
			  NULL, 0, NULL,
			  handle->callback_data);

	if (handle->idle_tag) {
		/* We already resolved, but the idle hasn't run yet */
		g_source_remove (handle->idle_tag);
	} else {
		/* TODO: We shouldn't get any callbacks after stopping,
		   but there is a bug in howl 0.9.5 where it can still
		   happen */
		sw_discovery_cancel (get_global_howl_session (),
				     handle->howl_id);
	}
	
	free_resolve_handle (handle);
	
	return FALSE;
}

#endif

/**
 * gnome_vfs_dns_sd_resolve:
 * @handle: pointer to a pointer to a #GnomeVFSDNSSDResolveHandle object.
 * @name: name of the service to resolve in UTF-8 encoding.
 * @type: type of the service to resolve.
 * @domain: dns domain of the service to resolve, or "local" for multicast DNS.
 * @timeout: maximum time (in milliseconds) to try to resolve, or zero if no maximum.
 * @callback: function to be called when the service has been resolved.
 * @callback_data: data to pass to @callback.
 * @callback_data_destroy_func: optional destructor function for @callback_data.
 *
 * Tries to resolve a specific service (typically recieved from
 * gnome_vfs_dns_sd_browse()) into a hostname/ip, port number and additional
 * options.
 *
 * If you ever have to save a reference to a service you should store the
 * unresolved name/type/domain tripplet, because the actual host for the
 * service can change.
 *
 * The @timeout argument is primarily useful for local resolves, since the
 * host owning the service might no longer be around to answer.
 * 
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult			      
gnome_vfs_dns_sd_resolve (GnomeVFSDNSSDResolveHandle **handle_out,
			  const char *name,
			  const char *type,
			  const char *domain,
			  int timeout,
			  GnomeVFSDNSSDResolveCallback callback,
			  gpointer callback_data,
			  GDestroyNotify callback_data_destroy_func)
{
	GnomeVFSDNSSDResolveHandle *handle;

	*handle_out = NULL;
	
	handle = g_new0 (GnomeVFSDNSSDResolveHandle, 1);
	handle->name = g_strdup (name);
	handle->domain = g_strdup (domain);
	handle->type = g_strdup (type);
	handle->callback = callback;
	handle->callback_data = callback_data;
	handle->callback_data_destroy_func = callback_data_destroy_func;
	
	if (strcmp (domain, "local") == 0) {
#ifdef HAVE_AVAHI
		AvahiClient *client;
		AvahiServiceResolver *sr;

		handle->is_local = TRUE;
		client = get_global_avahi_client ();
		if (client) {
			sr = avahi_service_resolver_new (client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 
							 name, type, domain, AVAHI_PROTO_UNSPEC, 
							 AVAHI_LOOKUP_NO_ADDRESS,
							 avahi_resolve_async_callback, handle);
			if (sr != NULL) {
				handle->avahi_sr = sr;
				*handle_out = handle;
				return GNOME_VFS_OK;
			}
		}
		return GNOME_VFS_ERROR_GENERIC;
#elif defined (HAVE_HOWL)
		sw_result res;
		sw_discovery session;
		
		handle->is_local = TRUE;

		session = get_global_howl_session ();
		if (session) {
			res = sw_discovery_resolve (session,
						    0, 
						    name,
						    type,
						    domain,
						    howl_resolve_reply,
						    handle,
						    &handle->howl_id);
			if (res == SW_OKAY) {
				if (timeout != 0) {
					handle->timeout_tag = g_timeout_add (timeout,
									     howl_resolve_timeout,
									     handle);
				}
				
				*handle_out = handle;
				return GNOME_VFS_OK;
			}
		}
		return GNOME_VFS_ERROR_GENERIC;
#else
		return GNOME_VFS_ERROR_NOT_SUPPORTED;
#endif
	} else {
		handle->is_local = FALSE;
		if (g_thread_create (unicast_resolve_thread, handle,
				     FALSE, NULL) == NULL) {
			g_free (handle->domain);
			g_free (handle->type);
			g_free (handle);
			return GNOME_VFS_ERROR_INTERNAL;
		}
		*handle_out = handle;
		return GNOME_VFS_OK;
	}
}

/**
 * gnome_vfs_dns_sd_cancel_resolve:
 * @handle: handle of the resolve operation to be cancelled.
 *
 * Cancels resolving a service started with gnome_vfs_dns_sd_resolve().
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult
gnome_vfs_dns_sd_cancel_resolve (GnomeVFSDNSSDResolveHandle *handle)
{
	if (handle->is_local) {
#ifdef HAVE_AVAHI
		avahi_service_resolver_free (handle->avahi_sr);
		free_resolve_handle (handle);
#endif
#ifdef HAVE_HOWL
		g_source_remove (handle->timeout_tag);
		if (handle->idle_tag) {
			/* We already resolved, but the idle hasn't run yet */
			g_source_remove (handle->idle_tag);
		} else {
			/* TODO: We shouldn't get any callbacks after stopping,
			   but there is a bug in howl 0.9.5 where it can still
			   happen */
			sw_discovery_cancel (get_global_howl_session (),
					     handle->howl_id);
		}
		free_resolve_handle (handle);
		
#endif
		return GNOME_VFS_OK;
	} else {
		handle->cancelled = TRUE;
		return GNOME_VFS_OK;
	}
}

#if defined(HAVE_AVAHI) || defined(HAVE_HOWL)
static int
find_existing_service (GArray *array,
		       const char *name,
		       const char *type,
		       const char *domain)
{
	GnomeVFSDNSSDService *existing;
	int i;
	
	for (i = 0; i < array->len; i++) {
		existing = &g_array_index (array, GnomeVFSDNSSDService, i);
		if (strcmp (existing->name, name) == 0 &&
		    strcmp (existing->type, type) == 0 &&
		    strcmp (existing->domain, domain) == 0) {
			return i;
		}
	}
	return -1;
		    
}
#endif


#ifdef HAVE_AVAHI
struct sync_browse_data {
	AvahiSimplePoll *poll;
	GArray *array;
};

static void
avahi_browse_sync_client_callback (AvahiClient *client, AvahiClientState state, void *user_data)
{
	struct sync_browse_data *data;

	data = user_data;
	if (state == AVAHI_CLIENT_FAILURE) {
		avahi_simple_poll_quit (data->poll);
	}
}

static void 
avahi_browse_sync_callback (AvahiServiceBrowser *b,
			    AvahiIfIndex interface,
			    AvahiProtocol protocol,
			    AvahiBrowserEvent event,
			    const char *name,
			    const char *type,
			    const char *domain,
			    AvahiLookupResultFlags flags,
			    void *user_data)
{
	struct sync_browse_data *data;
	GnomeVFSDNSSDService service, *existing;
	int i;
	gboolean free_service;

	data = user_data;
	
	free_service = TRUE;
	service.name = g_strdup (name);
	service.type = g_strdup (type);
	service.domain = g_strdup (domain);
	
	if (event == AVAHI_BROWSER_NEW) {
		if (find_existing_service (data->array, service.name, service.type,
					   service.domain) == -1) {
			free_service = FALSE;
			g_array_append_val (data->array, service);
		} 
	} else if (event == AVAHI_BROWSER_REMOVE) {
		i = find_existing_service (data->array, service.name, service.type,
					   service.domain);
		if (i != -1) {
			existing = &g_array_index (data->array, GnomeVFSDNSSDService, i);
			g_free (existing->name);
			g_free (existing->type);
			g_free (existing->domain);
			g_array_remove_index (data->array, i);
		}
	} else if (event == AVAHI_BROWSER_ALL_FOR_NOW) {
		avahi_simple_poll_quit (data->poll);
	}


	if (free_service) {
		g_free (service.name);
		g_free (service.type);
		g_free (service.domain);
	}	
}

static void
stop_poll_timeout (AvahiTimeout *timeout, void *user_data)
{
	AvahiSimplePoll *poll = user_data;
	
	avahi_simple_poll_quit (poll);
}

#endif


#ifdef HAVE_HOWL


static sw_result
howl_browse_reply_sync (sw_discovery                  discovery,
			sw_discovery_oid              id,
			sw_discovery_browse_status    status,
			sw_uint32		      interface_index,
			sw_const_string               name,
			sw_const_string               type,
			sw_const_string               domain,
			sw_opaque                     extra)
{
	GnomeVFSDNSSDService service, *existing;
	GArray *array;
	int i, len;
	gboolean free_service;

	array = extra;
	
	if (status == SW_DISCOVERY_BROWSE_RELEASE) {
		/* free in an idle to make sure the other idles are done,
		   and to give sane environment for destroy callback */
		return SW_OKAY;
	}

	free_service = TRUE;
	service.name = g_strdup (name);
	service.type = g_strdup (type);
	service.domain = g_strdup (domain);
	
	/* We don't want last dots in the domain or type */
	len = strlen (service.type);
	if (len > 0 && service.type[len-1] == '.')
		service.type[len-1] = 0;
	len = strlen (service.domain);
	if (len > 0 && service.domain[len-1] == '.')
		service.domain[len-1] = 0;
	
	if (status == SW_DISCOVERY_BROWSE_ADD_SERVICE) {
		if (find_existing_service (array, service.name, service.type,
					   service.domain) == -1) {
			free_service = FALSE;
			g_array_append_val (array, service);
		} 
	} else if (status == SW_DISCOVERY_BROWSE_REMOVE_SERVICE) {
		i = find_existing_service (array, service.name, service.type,
					   service.domain);
		if (i != -1) {
			existing = &g_array_index (array, GnomeVFSDNSSDService, i);
			g_free (existing->name);
			g_free (existing->type);
			g_free (existing->domain);
			g_array_remove_index (array, i);
		}
	} else {
		g_warning ("Unknown browse status\n");
	}
	
	if (free_service) {
		g_free (service.name);
		g_free (service.type);
		g_free (service.domain);
	}	
	return SW_OKAY;
}
#endif /* HAVE_HOWL */

/**
 * gnome_vfs_dns_sd_browse_sync:
 * @domain: The dns domain to browse, or "local" for multicast DNS.
 * @type: type of the service to browse for.
 * @timeout_msec: maximum time to browse, in milliseconds.
 * @n_services: pointer to location to store number of returned services.
 * @services: pointer to location to store returned services.
 *
 * Browses @domain for service of the type @type, returning the result
 * after blocking for the duration of the browse. For details about @domain
 * and @type, see gnome_vfs_dns_sd_browse().
 *
 * @timeout is essential for the "local" domain, since you can never really
 * know when you've gotten the full set of return values when using multicast.
 *
 * The returned list can be freed with gnome_vfs_dns_sd_service_list_free().
 * 
 * This is a synchronous version of gnome_vfs_dns_sd_browse(), see that for
 * more details.
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult
gnome_vfs_dns_sd_browse_sync (const char *domain,
			      const char *type,
			      int timeout_msec,
			      int *n_services,
			      GnomeVFSDNSSDService **services)
{
	*n_services = 0;
	*services = NULL;
	
	if (strcmp (domain, "local") == 0) {
#ifdef HAVE_AVAHI
		AvahiSimplePoll *simple_poll;
		const AvahiPoll *poll;
		AvahiClient *client = NULL;
		AvahiServiceBrowser *sb;
		int error;
		GArray *array;
		struct sync_browse_data data;
		struct timeval tv;

		simple_poll = avahi_simple_poll_new ();
		data.poll = simple_poll;
		if (simple_poll == NULL) {
			g_warning ("Failed to create simple poll object");
			return GNOME_VFS_ERROR_GENERIC;
		}

		poll = avahi_simple_poll_get (simple_poll);
		client = avahi_client_new (poll, 0,
					   avahi_browse_sync_client_callback, &data, &error);
		
		/* Check wether creating the client object succeeded */
		if (client == NULL) {
			g_warning ("Failed to create client: %s\n", avahi_strerror (error));
			avahi_simple_poll_free (simple_poll);
			return GNOME_VFS_ERROR_GENERIC;
		}


		array = g_array_new (FALSE, FALSE, sizeof (GnomeVFSDNSSDService));
		data.array = array;
		sb = avahi_service_browser_new (client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, type, NULL, 
						AVAHI_LOOKUP_USE_MULTICAST, avahi_browse_sync_callback, &data);
		if (sb == NULL) {
			g_warning ("Failed to create service browser: %s\n", avahi_strerror (avahi_client_errno (client)));
			g_array_free (array, TRUE);
			avahi_client_free (client);
			avahi_simple_poll_free (simple_poll);
			return GNOME_VFS_ERROR_GENERIC;
		}


		avahi_elapse_time (&tv, timeout_msec,  0);
		poll->timeout_new (poll, &tv, stop_poll_timeout, (void *)simple_poll);

		/* Run the main loop util reply or timeout */
		for (;;)
			if (avahi_simple_poll_iterate (simple_poll, -1) != 0)
				break;

		
		avahi_service_browser_free (sb);
		avahi_client_free (client);
		avahi_simple_poll_free (simple_poll);

		*n_services = array->len;
		*services = (GnomeVFSDNSSDService *)g_array_free (array, FALSE);
		
		return GNOME_VFS_OK;
#elif defined (HAVE_HOWL)
		sw_discovery session;
		sw_salt salt;
		sw_result res;
		sw_ulong timeout;
		sw_discovery_oid browse_id;
		struct timeval end_tv, tv;
		GArray *array;

		if (sw_discovery_init (&session) != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_browse_sync - howl init failed\n");
			return GNOME_VFS_ERROR_GENERIC;
		}

		if (sw_discovery_salt (session, &salt) != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_browse_sync - couldn't get salt\n");
			sw_discovery_fina (session);
			return GNOME_VFS_ERROR_GENERIC;
		}
		
		array = g_array_new (FALSE, FALSE, sizeof (GnomeVFSDNSSDService));
		res = sw_discovery_browse (session,
					   0,
					   type, domain,
					   howl_browse_reply_sync,
					   array,
					   &browse_id);
		if (res != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_browse_sync - howl browse failed\n");
			g_array_free (array, TRUE);
			sw_discovery_fina (session);
			return GNOME_VFS_ERROR_GENERIC;
		}
		
		gettimeofday (&end_tv, NULL);
		tv = end_tv;
		
		end_tv.tv_sec += timeout_msec / 1000;
		end_tv.tv_usec += (timeout_msec % 1000) * 1000;
		end_tv.tv_sec += end_tv.tv_usec / 1000000;
		end_tv.tv_usec %= 1000000;
		
		do {
			timeout = timeout_msec;
			sw_salt_step (salt, &timeout);

			gettimeofday (&tv, NULL);
			timeout_msec = (end_tv.tv_sec - tv.tv_sec) * 1000 + 
				(end_tv.tv_usec - tv.tv_usec) / 1000;
		} while (timeout_msec > 0);
		
		sw_discovery_cancel (session, browse_id);
					  
		sw_discovery_fina (session);

		*n_services = array->len;
		*services = (GnomeVFSDNSSDService *)g_array_free (array, FALSE);
		
		return GNOME_VFS_OK;
#else
		return GNOME_VFS_ERROR_NOT_SUPPORTED;
#endif
	} else {
		return unicast_browse_sync (domain, type,
					    n_services,
					    services);
	}
}

#ifdef HAVE_AVAHI
static void
avahi_resolve_sync_callback (AvahiServiceResolver *r,
			     AvahiIfIndex interface,
			     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 flags,
			     void *user_data)
{
	struct sync_resolve_data *data;

	data = user_data;
	if (event == AVAHI_RESOLVER_FOUND) {
		data->got_data = TRUE;
		data->protocol = protocol;
		data->interface = interface;
		data->host = g_strdup (host_name);
		data->port = port;
		data->text_len = avahi_string_list_serialize (txt, NULL, 0);
		data->text = g_malloc (data->text_len);
		avahi_string_list_serialize (txt, data->text, data->text_len);
	}
	
	avahi_service_resolver_free (r);
        avahi_simple_poll_quit (data->poll);
}

#endif

#ifdef HAVE_HOWL
struct sync_resolve_data {
	gboolean got_data;
	char *host;
	int port;
	char *text;
	int text_len;
};

static sw_result
howl_resolve_reply_sync (sw_discovery                   discovery,
			 sw_discovery_oid               id,
			 sw_uint32	                interface_index,
			 sw_const_string                name,
			 sw_const_string                type,
			 sw_const_string                domain,
			 sw_ipv4_address                address,
			 sw_port                        port,
			 sw_octets                      text_record,
			 sw_ulong                       text_record_len,
			 sw_opaque                      extra)
{
	struct sync_resolve_data *data;

	data = extra;
	data->got_data = TRUE;
	data->host = g_malloc (16);
	sw_ipv4_address_name (address, data->host, 16);
	data->port = port;
	data->text = g_memdup (text_record, text_record_len);
	data->text_len = text_record_len;
	
	return SW_OKAY;
}
#endif

/**
 * gnome_vfs_dns_sd_resolve_sync:
 * @name: name of the service to resolve in UTF-8 encoding.
 * @type: type of the service to resolve.
 * @domain: dns domain of the service to resolve, or "local" for multicast DNS.
 * @timeout_msec: maximum time(in milliseconds) to try to resolve.
 * @host: location to store the host name or ip of the host hosting the service.
 * @port: location to store the port number to use for the service.
 * @text: location to store a hash table giving additional options about the service.
 * @text_raw_len_out: location to store length of @text_raw_out.
 * @text_raw_out: location to store raw version of the additional options in @text.
 *
 * Tries to resolve a specific service (typically recieved from
 * gnome_vfs_dns_sd_browse()) into a hostname/ip, port number and additional
 * options.
 *
 * This is a synchronous version of gnome_vfs_dns_sd_resolve(), see that (and
 * its callback GnomeVFSDNSSDResolveCallback()) for more details.
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult			      
gnome_vfs_dns_sd_resolve_sync (const char *name,
			       const char *type,
			       const char *domain,
			       int timeout_msec,
			       char **host,
			       int *port,
			       GHashTable **text,
			       int *text_raw_len_out,
			       char **text_raw_out)
{
	int text_raw_len;
	char *text_raw;
	GnomeVFSResult res;
	
	if (strcmp (domain, "local") == 0) {
#ifdef HAVE_AVAHI
		AvahiSimplePoll *simple_poll;
		AvahiClient *client = NULL;
		AvahiServiceResolver *sr;
		int error;
		struct sync_resolve_data resolve_data = {0};
		AvahiLookupFlags flags = AVAHI_LOOKUP_NO_ADDRESS;

		simple_poll = avahi_simple_poll_new ();
		resolve_data.poll = simple_poll;
		if (simple_poll == NULL) {
			g_warning ("Failed to create simple poll object");
			return GNOME_VFS_ERROR_GENERIC;
		}

		client = avahi_client_new (avahi_simple_poll_get (simple_poll), 0, 
					   avahi_resolve_sync_client_callback, &resolve_data, &error);
		
		/* Check wether creating the client object succeeded */
		if (client == NULL) {
			g_warning ("Failed to create client: %s\n", avahi_strerror (error));
			avahi_simple_poll_free (simple_poll);
			return GNOME_VFS_ERROR_GENERIC;
		}

		if (!text && !text_raw_out)
			flags|= AVAHI_LOOKUP_NO_TXT;

		sr = avahi_service_resolver_new (client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 
						 name, type, domain, AVAHI_PROTO_UNSPEC, 
						 flags, avahi_resolve_sync_callback, &resolve_data);
		if (sr == NULL) {
			g_warning ("Failed to resolve service '%s': %s\n", name, avahi_strerror (avahi_client_errno (client)));
			avahi_client_free (client);
			avahi_simple_poll_free (simple_poll);
			return GNOME_VFS_ERROR_GENERIC;
		}

		/* Run the main loop util reply or timeout */
		for (;;)
			if (avahi_simple_poll_iterate (simple_poll, -1) != 0)
				break;

		avahi_client_free (client);
		avahi_simple_poll_free (simple_poll);

		if (resolve_data.got_data) {
			GnomeVFSResult result = GNOME_VFS_OK;
			
			if (g_str_has_suffix (resolve_data.host, ".local")) {
				result = avahi_resolve_host_name (
					resolve_data.interface, resolve_data.protocol,
					resolve_data.host, host);
				g_free (resolve_data.host);
			} else {
				*host = resolve_data.host;
			}

			*port = resolve_data.port;

			if (text != NULL)
				*text = decode_txt_record (resolve_data.text, resolve_data.text_len);
			if (text_raw_len_out != NULL && text_raw_out) {
				*text_raw_len_out = resolve_data.text_len;
				*text_raw_out = resolve_data.text;
			} else {
				g_free (resolve_data.text);
			}
			return result;
		}
		
		return GNOME_VFS_ERROR_HOST_NOT_FOUND;
#elif defined (HAVE_HOWL)
		sw_discovery session;
		sw_salt salt;
		sw_result res;
		sw_ulong timeout;
		sw_discovery_oid resolve_id;
		struct timeval end_tv, tv;
		struct sync_resolve_data resolve_data = {0};
		
		if (sw_discovery_init (&session) != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_resolve_sync - howl init failed\n");
			return GNOME_VFS_ERROR_GENERIC;
		}

		if (sw_discovery_salt (session, &salt) != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_resolve_sync - couldn't get salt\n");
			sw_discovery_fina (session);
			return GNOME_VFS_ERROR_GENERIC;
		}
		
		res = sw_discovery_resolve (session,
					    0, 
					    name, type, domain,
					    howl_resolve_reply_sync,
					    &resolve_data,
					    &resolve_id);
		if (res != SW_OKAY) {
			g_warning ("gnome_vfs_dns_sd_resolve_sync - howl resolve failed\n");
			sw_discovery_fina (session);
			return GNOME_VFS_ERROR_GENERIC;
		}
		
		gettimeofday (&end_tv, NULL);
		tv = end_tv;
		
		end_tv.tv_sec += timeout_msec / 1000;
		end_tv.tv_usec += (timeout_msec % 1000) * 1000;
		end_tv.tv_sec += end_tv.tv_usec / 1000000;
		end_tv.tv_usec %= 1000000;
		
		do {
			timeout = timeout_msec;
			sw_salt_step (salt, &timeout);

			gettimeofday (&tv, NULL);
			timeout_msec = (end_tv.tv_sec - tv.tv_sec) * 1000 + 
				(end_tv.tv_usec - tv.tv_usec) / 1000;
		} while (!resolve_data.got_data && timeout_msec > 0);
		
		sw_discovery_cancel (session, resolve_id);
					  
		sw_discovery_fina (session);

		if (resolve_data.got_data) {
			*host = resolve_data.host;
			*port = resolve_data.port;
			if (text != NULL)
				*text = decode_txt_record (resolve_data.text, resolve_data.text_len);
			if (text_raw_len_out != NULL && text_raw_out) {
				*text_raw_len_out = resolve_data.text_len;
				*text_raw_out = resolve_data.text;
			} else {
				g_free (resolve_data.text);
			}
			return GNOME_VFS_OK;
		}
		
		return GNOME_VFS_ERROR_HOST_NOT_FOUND;
#else
		return GNOME_VFS_ERROR_NOT_SUPPORTED;
#endif
	} else {
		res = unicast_resolve_sync (name, type, domain,
					    host, port,
					    &text_raw_len, &text_raw);

		if (res == GNOME_VFS_OK) {
			if (text != NULL) {
				*text = decode_txt_record (text_raw, text_raw_len);
			}
			
			if (text_raw_len_out != NULL) {
				*text_raw_len_out = text_raw_len;
				*text_raw_out = text_raw;
			} else {
				g_free (text_raw);
			}
		}

		return res;
	}
}

/**
 * gnome_vfs_dns_sd_service_list_free:
 * @services: the list of services to free.
 * @n_services: the number of services to free.
 *
 * Frees a list of services as returned by gnome_vfs_dns_sd_browse_sync().
 */
void
gnome_vfs_dns_sd_service_list_free (GnomeVFSDNSSDService *services,
				    int n_services)
{
	int i;
	
	for (i = 0; i < n_services; i++) {
		g_free (services[i].name);
		g_free (services[i].type);
		g_free (services[i].domain);
	}
	g_free (services);
}


/**
 * gnome_vfs_dns_sd_list_browse_domains_sync:
 * @domain: the domain to list browsable domains in.
 * @timeout_msec: maximum time to run, in milliseconds.
 * @domains: location to store the returned list of domain names strings.
 *
 * Lists the recommended browsing domains for a specific dns domain.
 * This can be used to find interesting domains for the domain
 * you are currently in. These can then be browsed with gnome_vfs_dns_sd_browse().
 *
 * Return value: an integer representing the result of the operation.
 */
GnomeVFSResult
gnome_vfs_dns_sd_list_browse_domains_sync (const char *domain,
					   int timeout_msec,
					   GList **domains)
{
	if (strcmp (domain, "local") == 0) {
		/* TODO: Not supported at the moment */
		return GNOME_VFS_ERROR_NOT_SUPPORTED;
	} else {
		return unicast_list_domains_sync (domain, domains);
	}
}

/**
 * gnome_vfs_get_default_browse_domains:
 *
 * Returns a list of domain names that is useful to
 * browse for standard services. The list is generated
 * by contacting the dns server of the domain part the
 * hostname and asking for the list of browse domains.
 * Then extra domains from a gconf setting is added.
 *
 * The "local" domain is not normally returned by this.
 * Care should be taken with local services so that its
 * obvious that they are local, and cannot be confused
 * with non-local services.
 *
 * Return value: a #GList of domain name strings.
 */
GList *
gnome_vfs_get_default_browse_domains (void)
{
	char hostname[256];
	char *domain, *dot;
	GList *domains;
	char *extra_domains;
	char **domainsv;
	GConfClient *client;
	int i;
	
	domain = NULL;
	if (gethostname (hostname, sizeof(hostname)) == 0) {
		dot = strchr (hostname, '.');
		if (dot != NULL &&
		    dot[0] != 0 &&
		    dot[1] != 0) {
			domain = dot + 1;
		}
	}

	domains = NULL;
	if (domain != NULL) {
		gnome_vfs_dns_sd_list_browse_domains_sync (domain,
							   2000,
							   &domains);
		
	}

	if (!gconf_is_initialized ()) {
		if (!gconf_init (0, NULL, NULL)) {
			return domains;
		}
	}

	client = gconf_client_get_default ();
	extra_domains = gconf_client_get_string (client, PATH_GCONF_GNOME_VFS_DNS_SD_EXTRA_DOMAINS, NULL);


	if (extra_domains != NULL) {
		domainsv = g_strsplit (extra_domains, ",", 0);
		
		for (i = 0; domainsv[i] != NULL; i++) {
			domains = g_list_prepend (domains, g_strdup (domainsv[i]));
		}
		
		g_strfreev (domainsv);
	}

	g_free (extra_domains);
	
	g_object_unref (G_OBJECT (client));

	return domains;
}
