
#include "announce.h"
#include "network.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

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

#include <avahi-common/alternative.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include <avahi-common/thread-watch.h>

#define SERVICE_TYPE "_sdlbomber._udp"

static AvahiClient* client = NULL;
static AvahiThreadedPoll* threaded_poll = NULL;
static AvahiEntryGroup* group = NULL;
static AvahiServiceBrowser* browser = NULL;

static char* name = NULL;

static uint16_t port = 0;
static uint32_t version = 0;

static gamelistentry buffer_glentries[10];
static int buffer_glsize = 0;
static char buffer_glchanged[10];

gamelistentry gamelistentries[10];
int gamelistsize = 0;

static void myerror(AvahiClient* c, const char* s) {
	fprintf(stderr, "Error in: %s\n (%s)", s, avahi_strerror(avahi_client_errno(c)));
}

static void create_services(AvahiClient* c);

static void entry_group_callback(AvahiEntryGroup* g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void* userdata) {
	group = g;

	switch (state) {
	case AVAHI_ENTRY_GROUP_ESTABLISHED:
		break;

	case AVAHI_ENTRY_GROUP_COLLISION: {
		char* n = avahi_alternative_service_name(name);
		avahi_free(name);
		name = n;
	}

		/* And recreate the services */
		avahi_entry_group_reset(group);
		create_services(avahi_entry_group_get_client(g));
		break;

	case AVAHI_ENTRY_GROUP_FAILURE:
		fprintf(stderr, "Entry group failure: %s\n", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));

		avahi_threaded_poll_quit(threaded_poll);
		break;

	case AVAHI_ENTRY_GROUP_UNCOMMITED:
	case AVAHI_ENTRY_GROUP_REGISTERING:
		break;
	}
}

static void create_services(AvahiClient* c) {
	int ret;

	if (!group) {
		if (!(group = avahi_entry_group_new(c, entry_group_callback, NULL))) {
			myerror(c, "avahi_entry_group_new");
			goto fail;
		}
	}

again:
	if (avahi_entry_group_is_empty(group)) {
		char buf_version[128];
		snprintf(buf_version, sizeof(buf_version), "version=%X", (unsigned int) version);
		if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, name, SERVICE_TYPE, NULL, NULL, port, buf_version, NULL)) < 0) {

			if (ret == AVAHI_ERR_COLLISION) goto collision;

			fprintf(stderr, "Failed to add " SERVICE_TYPE ": %s\n", avahi_strerror(ret));
			goto fail;
		}

		if ((ret = avahi_entry_group_commit(group)) < 0) {
			fprintf(stderr, "Failed to commit entry group: %s\n", avahi_strerror(ret));
			goto fail;
		}
	}

	return;

collision: {
	char* n = avahi_alternative_service_name(name);
	avahi_free(name);
	name = n;
}
	avahi_entry_group_reset(group);
	goto again;

fail:
	avahi_threaded_poll_quit(threaded_poll);
}

static void client_callback(AvahiClient* c, AvahiClientState state, void* userdata) {
	(void) userdata;

	client = c;
	switch (state) {
	case AVAHI_CLIENT_S_RUNNING:
		if (port != 0) create_services(c);
		break;
	case AVAHI_CLIENT_FAILURE:
		myerror(c, "client failure");
		avahi_threaded_poll_quit(threaded_poll);
		break;
	case AVAHI_CLIENT_S_COLLISION:
	case AVAHI_CLIENT_S_REGISTERING:
		if (group) { avahi_entry_group_reset(group); }
		break;
	case AVAHI_CLIENT_CONNECTING:
		break;
	}
}

int registergame(const char* playername, uint16_t p, const unsigned char v[4]) {
	if (name) avahi_free(name);
	name = avahi_strdup(playername);

	port = p;
	memcpy(&version, v, 4);
	version = htonl(version);

	avahi_threaded_poll_lock(threaded_poll);
	create_services(client);
	avahi_threaded_poll_unlock(threaded_poll);

	return 1;
}

void unregistergame(void) {
	port = 0;

	avahi_threaded_poll_lock(threaded_poll);
	if (group) avahi_entry_group_reset(group);
	group = NULL;
	avahi_threaded_poll_unlock(threaded_poll);
}

static void remove_game_with_name(const char* name) {
	int i;

	for (i = 0; i < buffer_glsize; i++) {
		if (0 == strcmp(buffer_glentries[i].name, name)) {
			/* Remove it */
			buffer_glsize--;
			if (i != buffer_glsize) {
				buffer_glentries[i] = buffer_glentries[buffer_glsize];
				buffer_glchanged[i] = 1;
			}
		}
	}
}

static void resolve_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* userdata) {
	int i;
	uint32_t want_version;
	(void) interface;
	(void) host_name;
	(void) flags;
	(void) userdata;
	assert(r);

	if (protocol != AVAHI_PROTO_INET) goto done; /* ignore non IPv4 for now */
	if (buffer_glsize >= GAMELIST_MAXSIZE) goto done;

	memcpy(&want_version, gameversion, 4);
	want_version = htonl(want_version);

	/* Called whenever a service has been resolved successfully or timed out */

	switch (event) {
	case AVAHI_RESOLVER_FAILURE:
		fprintf(
			stderr,
			"(Resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s\n",
			name,
			type,
			domain,
			avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
		break;

	case AVAHI_RESOLVER_FOUND: {
		gamelistentry* ge;
		unsigned int version;
		int have_version = 0;
		AvahiStringList* psl;
		for (psl = txt; psl; psl = psl->next) {
			if (0 == strncmp("version=", (const char*) psl->text, 8)) {
				sscanf((const char*) psl->text, "version=%X", &version);
				if (version != want_version) goto done; /* version mismatch */
				have_version = 1;
			}
		}
		if (!have_version) goto done;
		remove_game_with_name(name);
		i = buffer_glsize++;
		ge = &buffer_glentries[i];
		buffer_glchanged[i] = 1;
		memset(ge, 0, sizeof(*ge));
		ge->netname.sin_addr.s_addr = address->data.ipv4.address;
		ge->netname.sin_family = AF_INET;
		ge->netname.sin_port = port;
		strncpy(ge->name, name, 15);
	}
	}

done:
	avahi_service_resolver_free(r);
}

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

	assert(b);

	/* Called whenever a new services becomes available on the LAN or is removed from the LAN */

	switch (event) {
	case AVAHI_BROWSER_FAILURE:

		fprintf(stderr, "(Browser) %s\n", avahi_strerror(avahi_client_errno(client)));
		avahi_threaded_poll_quit(threaded_poll);
		return;

	case AVAHI_BROWSER_NEW:
		/* fprintf(stderr, "(Browser) NEW: service '%s' of type '%s' in domain '%s'\n", name, type, domain); */

		/* We ignore the returned resolver object. In the callback
			function we free it. If the server is terminated before
			the callback function is called the server will free
			the resolver for us. */

		if (!(avahi_service_resolver_new(client, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, 0, resolve_callback, userdata)))
			fprintf(stderr, "Failed to resolve service '%s': %s\n", name, avahi_strerror(avahi_client_errno(client)));

		break;

	case AVAHI_BROWSER_REMOVE:
		remove_game_with_name(name);
		break;

	case AVAHI_BROWSER_ALL_FOR_NOW:
		break;
	case AVAHI_BROWSER_CACHE_EXHAUSTED:
		break;
	}
}

static void freefoundgames(void) {
	memset(gamelistentries, 0, sizeof(gamelistentries));
	memset(buffer_glentries, 0, sizeof(buffer_glentries));
	memset(buffer_glchanged, 0, sizeof(buffer_glchanged));

	gamelistsize = 0;
	buffer_glsize = 0;
}

int searchgames(void) {
	freefoundgames();

	avahi_threaded_poll_lock(threaded_poll);

	if (NULL == (browser = avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, SERVICE_TYPE, NULL, 0, browse_callback, NULL))) {
		fprintf(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_client_errno(client)));
		avahi_threaded_poll_unlock(threaded_poll);
		return 0;
	}

	avahi_threaded_poll_unlock(threaded_poll);

	usleep(200000);
	find_more_games();

	return 1;
}

int find_more_games(void) {
	int i, res = 0;

	avahi_threaded_poll_lock(threaded_poll);

	for (i = 0; i < buffer_glsize; i++) {
		if (!buffer_glchanged[i]) continue;
		buffer_glchanged[i] = 0;
		gamelistentries[i] = buffer_glentries[i];
		res = 1;
	}
	if (gamelistsize != buffer_glsize) {
		res = 1;
		gamelistsize = buffer_glsize;
	}

	avahi_threaded_poll_unlock(threaded_poll);

	return res;
}

void stop_search(void) {
	avahi_threaded_poll_lock(threaded_poll);
	avahi_service_browser_free(browser);
	avahi_threaded_poll_unlock(threaded_poll);
}

int initannouncer(void) {
	if (!(threaded_poll = avahi_threaded_poll_new())) {
		fprintf(stderr, "avahi_threaded_poll_new failed\n");
		return 0;
	}

	if (!(client = avahi_client_new(avahi_threaded_poll_get(threaded_poll), 0, client_callback, NULL, NULL))) {
		fprintf(stderr, "avahi_client_new failed\n");
		avahi_threaded_poll_free(threaded_poll);
		threaded_poll = NULL;
		return 0;
	}

	if (avahi_threaded_poll_start(threaded_poll) < 0) {
		fprintf(stderr, "avahi_threaded_poll_start failed\n");
		avahi_client_free(client);
		avahi_threaded_poll_free(threaded_poll);
		client = NULL;
		threaded_poll = NULL;
		return 0;
	}

	return 1;
}

void freeannouncer(void) {
	freefoundgames();

	avahi_threaded_poll_stop(threaded_poll);
	if (client) avahi_client_free(client);
	if (threaded_poll) avahi_threaded_poll_free(threaded_poll);
	client = NULL;
	threaded_poll = NULL;

	avahi_free(name);
	name = NULL;
}

#if 0
int openmatcher()
{
struct hostent *hostptr;
	if(matcheropened) return 1;
	hostptr=gethostbyname(mname);
	if(!hostptr)
	{
		hostptr=gethostbyaddr(mname,strlen(mname),AF_INET);
		if(!hostptr)
			return 0;
	}
	memset(&matchername,0,sizeof(matchername));
	matchername.sin_family=AF_INET;
	matchername.sin_port=htons(PORT);
	memcpy(&matchername.sin_addr,hostptr->h_addr,hostptr->h_length);
	matcheropened=1;
	return 1;
}

int registergame()
{
long now;
int size;
long lastreg;

	if(!openmatcher()) return 0;
	pulseoff();
	now=longtime();
	lastreg=now-1;
	while(longtime()-now<10)
	{
		if(longtime()-lastreg>=1)
		{
			lastreg=longtime();
			putmsg(&matchername,regpacket,REGISTERLEN);
		}
		size=getmsg(1000);
		if(size<REGISTERLEN+1) continue;
		if(mesg[0]!=PKT_ACK) continue;
		if(memcmp(regpacket,mesg+1,REGISTERLEN)) continue;
		return 1;
	}
	return 0;
}
int unregistergame()
{
long now;
int size;


	if(!openmatcher()) return 0;
	pulseoff();
	now=longtime();
	clearreg();
	while(longtime()-now<10)
	{
		putmsg(&matchername,regpacket,REGISTERLEN);
		size=getmsg(1000);
		if(size<REGISTERLEN+1) continue;
		if(mesg[0]!=PKT_ACK) continue;
		if(memcmp(regpacket,mesg+1,REGISTERLEN)) continue;
		return 1;
	}
	return 0;

}
#endif
