/*
 *  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 2 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 Library General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
 */

#include "internal.h"

#include "mdns_interface.h"
#include "debug.h"
#include "buddy.h"
#include "bonjour.h"

#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-client/publish.h>

#include <avahi-common/address.h>
#include <avahi-common/malloc.h>
#include <avahi-common/error.h>
#include <avahi-common/strlst.h>

#include <avahi-glib/glib-malloc.h>
#include <avahi-glib/glib-watch.h>

/* Avahi only defines the types that it actually uses (which at this time doesn't include NULL) */
#ifndef AVAHI_DNS_TYPE_NULL
#define AVAHI_DNS_TYPE_NULL 0x0A
#endif

/* data used by avahi bonjour implementation */
typedef struct _avahi_session_impl_data {
	AvahiClient *client;
	AvahiGLibPoll *glib_poll;
	AvahiServiceBrowser *sb;
	AvahiEntryGroup *group;
	AvahiEntryGroup *buddy_icon_group;
} AvahiSessionImplData;

typedef struct _avahi_buddy_service_resolver_data {
	AvahiServiceResolver *resolver;
	AvahiIfIndex interface;
	AvahiProtocol protocol;
	gchar *name;
	gchar *type;
	gchar *domain;
	/* This is a reference to the entry in BonjourBuddy->ips */
	const char *ip;
} AvahiSvcResolverData;

typedef struct _avahi_buddy_impl_data {
	GSList *resolvers;
	AvahiRecordBrowser *buddy_icon_rec_browser;
} AvahiBuddyImplData;

static gint
_find_resolver_data(gconstpointer a, gconstpointer b) {
	const AvahiSvcResolverData *rd_a = a;
	const AvahiSvcResolverData *rd_b = b;
	gint ret = 1;

	if(rd_a->interface == rd_b->interface
			&& rd_a->protocol == rd_b->protocol
			&& !strcmp(rd_a->name, rd_b->name)
			&& !strcmp(rd_a->type, rd_b->type)
			&& !strcmp(rd_a->domain, rd_b->domain)) {
		ret = 0;
	}

	return ret;
}

static gint
_find_resolver_data_by_resolver(gconstpointer a, gconstpointer b) {
	const AvahiSvcResolverData *rd_a = a;
	const AvahiServiceResolver *resolver = b;
	gint ret = 1;

	if(rd_a->resolver == resolver)
		ret = 0;

	return ret;
}

static void
_cleanup_resolver_data(AvahiSvcResolverData *rd) {
	if (rd->resolver)
		avahi_service_resolver_free(rd->resolver);
	g_free(rd->name);
	g_free(rd->type);
	g_free(rd->domain);
	g_free(rd);
}


static void
_resolver_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
		  AvahiResolverEvent event, const char *name, const char *type, const char *domain,
		  const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
		  AvahiLookupResultFlags flags, void *userdata) {

	PurpleBuddy *pb;
	BonjourBuddy *bb;
	PurpleAccount *account = userdata;
	AvahiStringList *l;
	size_t size;
	char *key, *value;
	int ret;
	char ip[AVAHI_ADDRESS_STR_MAX];
	AvahiBuddyImplData *b_impl;
	AvahiSvcResolverData *rd;
	GSList *res;

	g_return_if_fail(r != NULL);

	pb = purple_find_buddy(account, name);
	bb = (pb != NULL) ? pb->proto_data : NULL;

	switch (event) {
		case AVAHI_RESOLVER_FAILURE:
			purple_debug_error("bonjour", "_resolve_callback - Failure: %s\n",
				avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));

			avahi_service_resolver_free(r);
			if (bb != NULL) {
				b_impl = bb->mdns_impl_data;
				res = g_slist_find_custom(b_impl->resolvers, r, _find_resolver_data_by_resolver);
				if (res != NULL) {
					rd = res->data;
					b_impl->resolvers = g_slist_remove_link(b_impl->resolvers, res);

					/* We've already freed the resolver */
					rd->resolver = NULL;
					_cleanup_resolver_data(rd);

					/* If this was the last resolver, remove the buddy */
					if (b_impl->resolvers == NULL) {
						purple_account_remove_buddy(account, pb, NULL);
						purple_blist_remove_buddy(pb);
					}
				}
			}
			break;
		case AVAHI_RESOLVER_FOUND:
			/* create a buddy record */
			if (bb == NULL)
				bb = bonjour_buddy_new(name, account);
			b_impl = bb->mdns_impl_data;

			/* If we're reusing an existing buddy, it may be a new resolver or an existing one. */
			res = g_slist_find_custom(b_impl->resolvers, r, _find_resolver_data_by_resolver);
			if (res != NULL)
				rd = res->data;
			else {
				rd = g_new0(AvahiSvcResolverData, 1);
				rd->resolver = r;
				rd->interface = interface;
				rd->protocol = protocol;
				rd->name = g_strdup(name);
				rd->type = g_strdup(type);
				rd->domain = g_strdup(domain);

				b_impl->resolvers = g_slist_prepend(b_impl->resolvers, rd);
			}


			/* Get the ip as a string */
			avahi_address_snprint(ip, AVAHI_ADDRESS_STR_MAX, a);

			if (rd->ip == NULL || strcmp(rd->ip, ip) != 0) {
				/* We store duplicates in bb->ips, so we always remove the one */
				if (rd->ip != NULL) {
					bb->ips = g_slist_remove(bb->ips, rd->ip);
					g_free((gchar *) rd->ip);
				}
				bb->ips = g_slist_prepend(bb->ips, g_strdup(ip));
				rd->ip = bb->ips->data;
			}

			bb->port_p2pj = port;

			/* Obtain the parameters from the text_record */
			clear_bonjour_buddy_values(bb);
			for(l = txt; l != NULL; l = l->next) {
				if ((ret = avahi_string_list_get_pair(l, &key, &value, &size)) < 0)
					continue;
				set_bonjour_buddy_value(bb, key, value, size);
				/* TODO: Since we're using the glib allocator, I think we
				 * can use the values instead of re-copying them */
				avahi_free(key);
				avahi_free(value);
			}

			if (!bonjour_buddy_check(bb)) {
				_cleanup_resolver_data(rd);
				b_impl->resolvers = g_slist_remove(b_impl->resolvers, rd);
				/* If this was the last resolver, remove the buddy */
				if (b_impl->resolvers == NULL) {
					if (pb != NULL) {
						purple_account_remove_buddy(account, pb, NULL);
						purple_blist_remove_buddy(pb);
					} else
						bonjour_buddy_delete(bb);
				}
			} else
				/* Add or update the buddy in our buddy list */
				bonjour_buddy_add_to_purple(bb, pb);

			break;
		default:
			purple_debug_info("bonjour", "Unrecognized Service Resolver event: %d.\n", event);
	}

}

static void
_browser_callback(AvahiServiceBrowser *b, AvahiIfIndex interface,
		  AvahiProtocol protocol, AvahiBrowserEvent event,
		  const char *name, const char *type, const char *domain,
		  AvahiLookupResultFlags flags, void *userdata) {

	PurpleAccount *account = userdata;
	PurpleBuddy *pb = NULL;

	switch (event) {
		case AVAHI_BROWSER_FAILURE:
			purple_debug_error("bonjour", "_browser_callback - Failure: %s\n",
				avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))));
			/* TODO: This is an error that should be handled. */
			break;
		case AVAHI_BROWSER_NEW:
			/* A new peer has joined the network and uses iChat bonjour */
			purple_debug_info("bonjour", "_browser_callback - new service\n");
			/* Make sure it isn't us */
			if (purple_utf8_strcasecmp(name, account->username) != 0) {
				if (!avahi_service_resolver_new(avahi_service_browser_get_client(b),
						interface, protocol, name, type, domain, AVAHI_PROTO_INET,
						0, _resolver_callback, account)) {
					purple_debug_warning("bonjour", "_browser_callback -- Error initiating resolver: %s\n",
						avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))));
				}
			}
			break;
		case AVAHI_BROWSER_REMOVE:
			purple_debug_info("bonjour", "_browser_callback - Remove service\n");
			pb = purple_find_buddy(account, name);
			if (pb != NULL) {
				BonjourBuddy *bb = pb->proto_data;
				AvahiBuddyImplData *b_impl;
				GSList *l;
				AvahiSvcResolverData *rd_search;

				g_return_if_fail(bb != NULL);

				b_impl = bb->mdns_impl_data;

				/* There may be multiple presences, we should only get rid of this one */

				rd_search = g_new0(AvahiSvcResolverData, 1);
				rd_search->interface = interface;
				rd_search->protocol = protocol;
				rd_search->name = (gchar *) name;
				rd_search->type = (gchar *) type;
				rd_search->domain = (gchar *) domain;

				l = g_slist_find_custom(b_impl->resolvers, rd_search, _find_resolver_data);

				g_free(rd_search);

				if (l != NULL) {
					AvahiSvcResolverData *rd = l->data;
					b_impl->resolvers = g_slist_remove(b_impl->resolvers, rd);
					/* This IP is no longer available */
					if (rd->ip != NULL) {
						bb->ips = g_slist_remove(bb->ips, rd->ip);
						g_free((gchar *) rd->ip);
					}
					_cleanup_resolver_data(rd);

					/* If this was the last resolver, remove the buddy */
					if (b_impl->resolvers == NULL) {
						purple_account_remove_buddy(account, pb, NULL);
						purple_blist_remove_buddy(pb);
					}
				}
			}
			break;
		case AVAHI_BROWSER_ALL_FOR_NOW:
		case AVAHI_BROWSER_CACHE_EXHAUSTED:
			break;
		default:
			purple_debug_info("bonjour", "Unrecognized Service browser event: %d.\n", event);
	}
}

static void
_buddy_icon_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
	BonjourDnsSd *data = userdata;
	AvahiSessionImplData *idata = data->mdns_impl_data;

	g_return_if_fail(g == idata->buddy_icon_group || idata->buddy_icon_group == NULL);

	switch(state) {
		case AVAHI_ENTRY_GROUP_ESTABLISHED:
			purple_debug_info("bonjour", "Successfully registered buddy icon data.\n");
			break;
		case AVAHI_ENTRY_GROUP_COLLISION:
			purple_debug_error("bonjour", "Collision registering buddy icon data.\n");
			break;
		case AVAHI_ENTRY_GROUP_FAILURE:
			purple_debug_error("bonjour", "Error registering buddy icon data: %s.\n",
				avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
			break;
		case AVAHI_ENTRY_GROUP_UNCOMMITED:
		case AVAHI_ENTRY_GROUP_REGISTERING:
			break;
	}

}

static void
_entry_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
	AvahiSessionImplData *idata = userdata;

	g_return_if_fail(g == idata->group || idata->group == NULL);

	switch(state) {
		case AVAHI_ENTRY_GROUP_ESTABLISHED:
			purple_debug_info("bonjour", "Successfully registered service.\n");
			break;
		case AVAHI_ENTRY_GROUP_COLLISION:
			purple_debug_error("bonjour", "Collision registering entry group.\n");
			/* TODO: Handle error - this should log out the account. (Possibly with "wants to die")*/
			break;
		case AVAHI_ENTRY_GROUP_FAILURE:
			purple_debug_error("bonjour", "Error registering entry group: %s\n.",
				avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
			/* TODO: Handle error - this should log out the account.*/
			break;
		case AVAHI_ENTRY_GROUP_UNCOMMITED:
		case AVAHI_ENTRY_GROUP_REGISTERING:
			break;
	}

}

static void
_buddy_icon_record_cb(AvahiRecordBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
		      AvahiBrowserEvent event, const char *name, uint16_t clazz, uint16_t type,
		      const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) {
	BonjourBuddy *buddy = userdata;
	AvahiBuddyImplData *idata = buddy->mdns_impl_data;

	switch (event) {
		case AVAHI_BROWSER_NEW:
			bonjour_buddy_got_buddy_icon(buddy, rdata, size);
			break;
		case AVAHI_BROWSER_REMOVE:
		case AVAHI_BROWSER_CACHE_EXHAUSTED:
		case AVAHI_BROWSER_ALL_FOR_NOW:
		case AVAHI_BROWSER_FAILURE:
			purple_debug_error("bonjour", "Error rerieving buddy icon record: %s\n",
				avahi_strerror(avahi_client_errno(avahi_record_browser_get_client(b))));
			break;
	}

	/* Stop listening */
	avahi_record_browser_free(b);
	if (idata->buddy_icon_rec_browser == b) {
		idata->buddy_icon_rec_browser = NULL;
	}
}

/****************************
 * mdns_interface functions *
 ****************************/

gboolean _mdns_init_session(BonjourDnsSd *data) {
	AvahiSessionImplData *idata = g_new0(AvahiSessionImplData, 1);
	const AvahiPoll *poll_api;
	int error;

	/* Tell avahi to use g_malloc and g_free */
	avahi_set_allocator (avahi_glib_allocator ());

	/* This currently depends on the glib mainloop,
	 * we should make it use the libpurple abstraction */

	idata->glib_poll = avahi_glib_poll_new(NULL, G_PRIORITY_DEFAULT);

	poll_api = avahi_glib_poll_get(idata->glib_poll);

	idata->client = avahi_client_new(poll_api, 0, NULL, data, &error);

	if (idata->client == NULL) {
		purple_debug_error("bonjour", "Error initializing Avahi: %s\n", avahi_strerror(error));
		avahi_glib_poll_free(idata->glib_poll);
		g_free(idata);
		return FALSE;
	}

	data->mdns_impl_data = idata;

	return TRUE;
}

gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) {
	int publish_result = 0;
	AvahiSessionImplData *idata = data->mdns_impl_data;
	AvahiStringList *lst = NULL;

	g_return_val_if_fail(idata != NULL, FALSE);

	if (!idata->group) {
		idata->group = avahi_entry_group_new(idata->client,
						     _entry_group_cb, idata);
		if (!idata->group) {
			purple_debug_error("bonjour",
				"Unable to initialize the data for the mDNS (%s).\n",
				avahi_strerror(avahi_client_errno(idata->client)));
			return FALSE;
		}
	}

	while (records) {
		PurpleKeyValuePair *kvp = records->data;
		lst = avahi_string_list_add_pair(lst, kvp->key, kvp->value);
		records = records->next;
	}

	/* Publish the service */
	switch (type) {
		case PUBLISH_START:
			publish_result = avahi_entry_group_add_service_strlst(
				idata->group, AVAHI_IF_UNSPEC,
				AVAHI_PROTO_INET, 0,
				purple_account_get_username(data->account),
				LINK_LOCAL_RECORD_NAME, NULL, NULL, data->port_p2pj, lst);
			break;
		case PUBLISH_UPDATE:
			publish_result = avahi_entry_group_update_service_txt_strlst(
				idata->group, AVAHI_IF_UNSPEC,
				AVAHI_PROTO_INET, 0,
				purple_account_get_username(data->account),
				LINK_LOCAL_RECORD_NAME, NULL, lst);
			break;
	}

	/* Free the memory used by temp data */
	avahi_string_list_free(lst);

	if (publish_result < 0) {
		purple_debug_error("bonjour",
			"Failed to add the " LINK_LOCAL_RECORD_NAME " service. Error: %s\n",
			avahi_strerror(publish_result));
		return FALSE;
	}

	if (type == PUBLISH_START
			&& (publish_result = avahi_entry_group_commit(idata->group)) < 0) {
		purple_debug_error("bonjour",
			"Failed to commit " LINK_LOCAL_RECORD_NAME " service. Error: %s\n",
			avahi_strerror(publish_result));
		return FALSE;
	}

	return TRUE;
}

gboolean _mdns_browse(BonjourDnsSd *data) {
	AvahiSessionImplData *idata = data->mdns_impl_data;

	g_return_val_if_fail(idata != NULL, FALSE);

	idata->sb = avahi_service_browser_new(idata->client, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, LINK_LOCAL_RECORD_NAME, NULL, 0, _browser_callback, data->account);
	if (!idata->sb) {

		purple_debug_error("bonjour",
			"Unable to initialize service browser.  Error: %s.\n",
			avahi_strerror(avahi_client_errno(idata->client)));
		return FALSE;
	}

	return TRUE;
}

gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) {
	AvahiSessionImplData *idata = data->mdns_impl_data;

	if (idata == NULL || idata->client == NULL)
		return FALSE;

	if (avatar_data != NULL) {
		gboolean new_group = FALSE;
		gchar *svc_name;
		int ret;
		AvahiPublishFlags flags = 0;

		if (idata->buddy_icon_group == NULL) {
			purple_debug_info("bonjour", "Setting new buddy icon.\n");
			new_group = TRUE;

			idata->buddy_icon_group = avahi_entry_group_new(idata->client,
				_buddy_icon_group_cb, data);
		} else {
			purple_debug_info("bonjour", "Updating existing buddy icon.\n");
			flags |= AVAHI_PUBLISH_UPDATE;
		}

		if (idata->buddy_icon_group == NULL) {
			purple_debug_error("bonjour",
				"Unable to initialize the buddy icon group (%s).\n",
				avahi_strerror(avahi_client_errno(idata->client)));
			return FALSE;
		}

		svc_name = g_strdup_printf("%s." LINK_LOCAL_RECORD_NAME "local",
				purple_account_get_username(data->account));

		ret = avahi_entry_group_add_record(idata->buddy_icon_group, AVAHI_IF_UNSPEC,
			AVAHI_PROTO_INET, flags, svc_name,
			AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 120, avatar_data, avatar_len);

		g_free(svc_name);

		if (ret < 0) {
			purple_debug_error("bonjour",
				"Failed to register buddy icon. Error: %s\n", avahi_strerror(ret));
			if (new_group) {
				avahi_entry_group_free(idata->buddy_icon_group);
				idata->buddy_icon_group = NULL;
			}
			return FALSE;
		}

		if (new_group && (ret = avahi_entry_group_commit(idata->buddy_icon_group)) < 0) {
			purple_debug_error("bonjour",
				"Failed to commit buddy icon group. Error: %s\n", avahi_strerror(ret));
			if (new_group) {
				avahi_entry_group_free(idata->buddy_icon_group);
				idata->buddy_icon_group = NULL;
			}
			return FALSE;
		}
	} else if (idata->buddy_icon_group != NULL) {
		purple_debug_info("bonjour", "Removing existing buddy icon.\n");
		avahi_entry_group_free(idata->buddy_icon_group);
		idata->buddy_icon_group = NULL;
	}

	return TRUE;
}

void _mdns_stop(BonjourDnsSd *data) {
	AvahiSessionImplData *idata = data->mdns_impl_data;

	if (idata == NULL || idata->client == NULL)
		return;

	if (idata->sb != NULL)
		avahi_service_browser_free(idata->sb);

	avahi_client_free(idata->client);
	avahi_glib_poll_free(idata->glib_poll);

	g_free(idata);

	data->mdns_impl_data = NULL;
}

void _mdns_init_buddy(BonjourBuddy *buddy) {
	buddy->mdns_impl_data = g_new0(AvahiBuddyImplData, 1);
}

void _mdns_delete_buddy(BonjourBuddy *buddy) {
	AvahiBuddyImplData *idata = buddy->mdns_impl_data;

	g_return_if_fail(idata != NULL);

	if (idata->buddy_icon_rec_browser != NULL)
		avahi_record_browser_free(idata->buddy_icon_rec_browser);

	while(idata->resolvers != NULL) {
		AvahiSvcResolverData *rd = idata->resolvers->data;
		_cleanup_resolver_data(rd);
		idata->resolvers = g_slist_delete_link(idata->resolvers, idata->resolvers);
	}

	g_free(idata);

	buddy->mdns_impl_data = NULL;
}

void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) {
	PurpleConnection *conn = purple_account_get_connection(buddy->account);
	BonjourData *bd = conn->proto_data;
	AvahiSessionImplData *session_idata = bd->dns_sd_data->mdns_impl_data;
	AvahiBuddyImplData *idata = buddy->mdns_impl_data;
	gchar *name;

	g_return_if_fail(idata != NULL);

	if (idata->buddy_icon_rec_browser != NULL)
		avahi_record_browser_free(idata->buddy_icon_rec_browser);

	purple_debug_info("bonjour", "Retrieving buddy icon for '%s'.\n", buddy->name);

	name = g_strdup_printf("%s." LINK_LOCAL_RECORD_NAME "local", buddy->name);
	idata->buddy_icon_rec_browser = avahi_record_browser_new(session_idata->client, AVAHI_IF_UNSPEC,
		AVAHI_PROTO_INET, name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 0,
		_buddy_icon_record_cb, buddy);
	g_free(name);

	if (!idata->buddy_icon_rec_browser) {
		purple_debug_error("bonjour",
			"Unable to initialize buddy icon record browser.  Error: %s.\n",
			avahi_strerror(avahi_client_errno(session_idata->client)));
	}

}

