/*
 * Copyright (c) 2007 Josef Cejka
 * Copyright (c) 2011 Jiri Svoboda
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @defgroup loc Location Service.
 * @brief HelenOS location service.
 * @{
 */

/** @file
 */

#include <ipc/services.h>
#include <ns.h>
#include <async.h>
#include <stdio.h>
#include <errno.h>
#include <stdbool.h>
#include <fibril_synch.h>
#include <macros.h>
#include <stdlib.h>
#include <str.h>
#include <ipc/loc.h>
#include <assert.h>

#include "category.h"
#include "locsrv.h"

#define NAME          "loc"
#define NULL_SERVICES  256

/** Callback session */
typedef struct {
	link_t cb_sess_list;
	async_sess_t *sess;
} cb_sess_t;

LIST_INITIALIZE(services_list);
LIST_INITIALIZE(namespaces_list);
LIST_INITIALIZE(servers_list);

/* Locking order:
 *  servers_list_mutex
 *  services_list_mutex
 *  (loc_server_t *)->services_mutex
 *  create_id_mutex
 **/

FIBRIL_MUTEX_INITIALIZE(services_list_mutex);
static FIBRIL_CONDVAR_INITIALIZE(services_list_cv);
static FIBRIL_MUTEX_INITIALIZE(servers_list_mutex);
static FIBRIL_MUTEX_INITIALIZE(create_id_mutex);
static FIBRIL_MUTEX_INITIALIZE(null_services_mutex);

static service_id_t last_id = 0;
static loc_service_t *null_services[NULL_SERVICES];

/*
 * Dummy list for null services. This is necessary so that null services can
 * be used just as any other services, e.g. in loc_service_unregister_core().
 */
static LIST_INITIALIZE(dummy_null_services);

/** Service directory ogranized by categories (yellow pages) */
static categ_dir_t cdir;

static FIBRIL_MUTEX_INITIALIZE(callback_sess_mutex);
static LIST_INITIALIZE(callback_sess_list);

service_id_t loc_create_id(void)
{
	/* TODO: allow reusing old ids after their unregistration
	 * and implement some version of LRU algorithm, avoid overflow
	 */
	
	fibril_mutex_lock(&create_id_mutex);
	last_id++;
	fibril_mutex_unlock(&create_id_mutex);
	
	return last_id;
}

/** Convert fully qualified service name to namespace and service name.
 *
 * A fully qualified service name can be either a plain service name
 * (then the namespace is considered to be an empty string) or consist
 * of two components separated by a slash. No more than one slash
 * is allowed.
 *
 */
static bool loc_fqsn_split(const char *fqsn, char **ns_name, char **name)
{
	size_t cnt = 0;
	size_t slash_offset = 0;
	size_t slash_after = 0;
	
	size_t offset = 0;
	size_t offset_prev = 0;
	wchar_t c;
	
	while ((c = str_decode(fqsn, &offset, STR_NO_LIMIT)) != 0) {
		if (c == '/') {
			cnt++;
			slash_offset = offset_prev;
			slash_after = offset;
		}
		offset_prev = offset;
	}
	
	/* More than one slash */
	if (cnt > 1)
		return false;
	
	/* No slash -> namespace is empty */
	if (cnt == 0) {
		*ns_name = str_dup("");
		if (*ns_name == NULL)
			return false;
		
		*name = str_dup(fqsn);
		if (*name == NULL) {
			free(*ns_name);
			return false;
		}
		
		if (str_cmp(*name, "") == 0) {
			free(*name);
			free(*ns_name);
			return false;
		}
		
		return true;
	}
	
	/* Exactly one slash */
	*ns_name = str_ndup(fqsn, slash_offset);
	if (*ns_name == NULL)
		return false;
	
	*name = str_dup(fqsn + slash_after);
	if (*name == NULL) {
		free(*ns_name);
		return false;
	}
	
	if (str_cmp(*name, "") == 0) {
		free(*name);
		free(*ns_name);
		return false;
	}
	
	return true;
}

/** Find namespace with given name. */
static loc_namespace_t *loc_namespace_find_name(const char *name)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));
	
	list_foreach(namespaces_list, namespaces, loc_namespace_t, namespace) {
		if (str_cmp(namespace->name, name) == 0)
			return namespace;
	}
	
	return NULL;
}

/** Find namespace with given ID.
 *
 * @todo: use hash table
 *
 */
static loc_namespace_t *loc_namespace_find_id(service_id_t id)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));
	
	list_foreach(namespaces_list, namespaces, loc_namespace_t, namespace) {
		if (namespace->id == id)
			return namespace;
	}
	
	return NULL;
}

/** Find service with given name. */
static loc_service_t *loc_service_find_name(const char *ns_name,
    const char *name)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));
	
	list_foreach(services_list, services, loc_service_t, service) {
		if ((str_cmp(service->namespace->name, ns_name) == 0)
		    && (str_cmp(service->name, name) == 0))
			return service;
	}
	
	return NULL;
}

/** Find service with given ID.
 *
 * @todo: use hash table
 *
 */
static loc_service_t *loc_service_find_id(service_id_t id)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));
	
	list_foreach(services_list, services, loc_service_t, service) {
		if (service->id == id)
			return service;
	}
	
	return NULL;
}

/** Create a namespace (if not already present). */
static loc_namespace_t *loc_namespace_create(const char *ns_name)
{
	loc_namespace_t *namespace;
	
	assert(fibril_mutex_is_locked(&services_list_mutex));
	
	namespace = loc_namespace_find_name(ns_name);
	if (namespace != NULL)
		return namespace;
	
	namespace = (loc_namespace_t *) malloc(sizeof(loc_namespace_t));
	if (namespace == NULL)
		return NULL;
	
	namespace->name = str_dup(ns_name);
	if (namespace->name == NULL) {
		free(namespace);
		return NULL;
	}
	
	namespace->id = loc_create_id();
	namespace->refcnt = 0;
	
	/*
	 * Insert new namespace into list of registered namespaces
	 */
	list_append(&(namespace->namespaces), &namespaces_list);
	
	return namespace;
}

/** Destroy a namespace (if it is no longer needed). */
static void loc_namespace_destroy(loc_namespace_t *namespace)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));

	if (namespace->refcnt == 0) {
		list_remove(&(namespace->namespaces));
		
		free(namespace->name);
		free(namespace);
	}
}

/** Increase namespace reference count by including service. */
static void loc_namespace_addref(loc_namespace_t *namespace,
    loc_service_t *service)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));

	service->namespace = namespace;
	namespace->refcnt++;
}

/** Decrease namespace reference count. */
static void loc_namespace_delref(loc_namespace_t *namespace)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));

	namespace->refcnt--;
	loc_namespace_destroy(namespace);
}

/** Unregister service and free it. */
static void loc_service_unregister_core(loc_service_t *service)
{
	assert(fibril_mutex_is_locked(&services_list_mutex));
	assert(fibril_mutex_is_locked(&cdir.mutex));
	
	loc_namespace_delref(service->namespace);
	list_remove(&(service->services));
	list_remove(&(service->server_services));
	
	/* Remove service from all categories. */
	while (!list_empty(&service->cat_memb)) {
		link_t *link = list_first(&service->cat_memb);
		svc_categ_t *memb = list_get_instance(link, svc_categ_t,
		    svc_link);
		fibril_mutex_lock(&memb->cat->mutex);
		category_remove_service(memb);
		fibril_mutex_unlock(&memb->cat->mutex);
	}
	
	free(service->name);
	free(service);
}

/**
 * Read info about new server and add it into linked list of registered
 * servers.
 */
static loc_server_t *loc_server_register(void)
{
	ipc_call_t icall;
	ipc_callid_t iid = async_get_call(&icall);
	
	if (IPC_GET_IMETHOD(icall) != LOC_SERVER_REGISTER) {
		async_answer_0(iid, EREFUSED);
		return NULL;
	}
	
	loc_server_t *server =
	    (loc_server_t *) malloc(sizeof(loc_server_t));
	if (server == NULL) {
		async_answer_0(iid, ENOMEM);
		return NULL;
	}
	
	/*
	 * Get server name
	 */
	int rc = async_data_write_accept((void **) &server->name, true, 0,
	    LOC_NAME_MAXLEN, 0, NULL);
	if (rc != EOK) {
		free(server);
		async_answer_0(iid, rc);
		return NULL;
	}
	
	/*
	 * Create connection to the server
	 */
	server->sess = async_callback_receive(EXCHANGE_SERIALIZE);
	if (!server->sess) {
		free(server->name);
		free(server);
		async_answer_0(iid, ENOTSUP);
		return NULL;
	}
	
	/*
	 * Initialize mutex for list of services
	 * supplied by this server
	 */
	fibril_mutex_initialize(&server->services_mutex);
	
	/*
	 * Initialize list of supplied services
	 */
	list_initialize(&server->services);
	link_initialize(&server->servers);
	
	fibril_mutex_lock(&servers_list_mutex);
	
	/* TODO:
	 * Check that no server with name equal to
	 * server->name is registered
	 */
	
	/*
	 * Insert new server into list of registered servers
	 */
	list_append(&(server->servers), &servers_list);
	fibril_mutex_unlock(&servers_list_mutex);
	
	async_answer_0(iid, EOK);
	
	return server;
}

/**
 * Unregister server, unregister all its services and free server
 * structure.
 *
 */
static int loc_server_unregister(loc_server_t *server)
{
	if (server == NULL)
		return EEXISTS;
	
	fibril_mutex_lock(&servers_list_mutex);
	
	if (server->sess)
		async_hangup(server->sess);
	
	/* Remove it from list of servers */
	list_remove(&(server->servers));
	
	/* Unregister all its services */
	fibril_mutex_lock(&services_list_mutex);
	fibril_mutex_lock(&server->services_mutex);
	fibril_mutex_lock(&cdir.mutex);
	
	while (!list_empty(&server->services)) {
		loc_service_t *service = list_get_instance(
		    list_first(&server->services), loc_service_t,
		    server_services);
		loc_service_unregister_core(service);
	}
	
	fibril_mutex_unlock(&cdir.mutex);
	fibril_mutex_unlock(&server->services_mutex);
	fibril_mutex_unlock(&services_list_mutex);
	fibril_mutex_unlock(&servers_list_mutex);
	
	/* Free name and server */
	if (server->name != NULL)
		free(server->name);
	
	free(server);
	
	return EOK;
}

/** Register service
 *
 */
static void loc_service_register(ipc_callid_t iid, ipc_call_t *icall,
    loc_server_t *server)
{
	if (server == NULL) {
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	/* Create new service entry */
	loc_service_t *service =
	    (loc_service_t *) malloc(sizeof(loc_service_t));
	if (service == NULL) {
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	/* Set the interface, if any. */
	service->forward_interface = IPC_GET_ARG1(*icall);

	/* Get fqsn */
	char *fqsn;
	int rc = async_data_write_accept((void **) &fqsn, true, 0,
	    LOC_NAME_MAXLEN, 0, NULL);
	if (rc != EOK) {
		free(service);
		async_answer_0(iid, rc);
		return;
	}
	
	char *ns_name;
	if (!loc_fqsn_split(fqsn, &ns_name, &service->name)) {
		free(fqsn);
		free(service);
		async_answer_0(iid, EINVAL);
		return;
	}
	
	free(fqsn);
	
	fibril_mutex_lock(&services_list_mutex);
	
	loc_namespace_t *namespace = loc_namespace_create(ns_name);
	free(ns_name);
	if (namespace == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		free(service->name);
		free(service);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	link_initialize(&service->services);
	link_initialize(&service->server_services);
	list_initialize(&service->cat_memb);
	
	/* Check that service is not already registered */
	if (loc_service_find_name(namespace->name, service->name) != NULL) {
		printf("%s: Service '%s/%s' already registered\n", NAME,
		    namespace->name, service->name);
		loc_namespace_destroy(namespace);
		fibril_mutex_unlock(&services_list_mutex);
		free(service->name);
		free(service);
		async_answer_0(iid, EEXISTS);
		return;
	}
	
	/* Get unique service ID */
	service->id = loc_create_id();

	loc_namespace_addref(namespace, service);
	service->server = server;
	
	/* Insert service into list of all services  */
	list_append(&service->services, &services_list);
	
	/* Insert service into list of services supplied by one server */
	fibril_mutex_lock(&service->server->services_mutex);
	
	list_append(&service->server_services, &service->server->services);
	
	fibril_mutex_unlock(&service->server->services_mutex);
	fibril_condvar_broadcast(&services_list_cv);
	fibril_mutex_unlock(&services_list_mutex);
	
	async_answer_1(iid, EOK, service->id);
}

/**
 *
 */
static void loc_service_unregister(ipc_callid_t iid, ipc_call_t *icall, 
    loc_server_t *server)
{
	loc_service_t *svc;
	
	fibril_mutex_lock(&services_list_mutex);
	svc = loc_service_find_id(IPC_GET_ARG1(*icall));
	if (svc == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	fibril_mutex_lock(&cdir.mutex);
	loc_service_unregister_core(svc);
	fibril_mutex_unlock(&cdir.mutex);
	fibril_mutex_unlock(&services_list_mutex);
	async_answer_0(iid, EOK);
}

static void loc_category_get_name(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	size_t act_size;
	category_t *cat;
	
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	fibril_mutex_lock(&cdir.mutex);
	
	cat = category_get(&cdir, IPC_GET_ARG1(*icall));
	if (cat == NULL) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, ENOENT);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	act_size = str_size(cat->name);
	if (act_size > size) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, EOVERFLOW);
		async_answer_0(iid, EOVERFLOW);
		return;
	}
	
	sysarg_t retval = async_data_read_finalize(callid, cat->name,
	    min(size, act_size));
	
	fibril_mutex_unlock(&cdir.mutex);
	
	async_answer_0(iid, retval);
}

static void loc_service_get_name(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	size_t act_size;
	loc_service_t *svc;
	char *fqn;
	
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	
	svc = loc_service_find_id(IPC_GET_ARG1(*icall));
	if (svc == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOENT);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	if (asprintf(&fqn, "%s/%s", svc->namespace->name, svc->name) < 0) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOMEM);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	act_size = str_size(fqn);
	if (act_size > size) {
		free(fqn);
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, EOVERFLOW);
		async_answer_0(iid, EOVERFLOW);
		return;
	}
	
	sysarg_t retval = async_data_read_finalize(callid, fqn,
	    min(size, act_size));
	free(fqn);
	
	fibril_mutex_unlock(&services_list_mutex);
	
	async_answer_0(iid, retval);
}

static void loc_service_get_server_name(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	size_t act_size;
	loc_service_t *svc;
	
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	
	svc = loc_service_find_id(IPC_GET_ARG1(*icall));
	if (svc == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOENT);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	if (svc->server == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, EINVAL);
		async_answer_0(iid, EINVAL);
		return;
	}
	
	act_size = str_size(svc->server->name);
	if (act_size > size) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, EOVERFLOW);
		async_answer_0(iid, EOVERFLOW);
		return;
	}
	
	sysarg_t retval = async_data_read_finalize(callid, svc->server->name,
	    min(size, act_size));
	
	fibril_mutex_unlock(&services_list_mutex);
	
	async_answer_0(iid, retval);
}

/** Connect client to the service.
 *
 * Find server supplying requested service and forward
 * the message to it.
 *
 */
static void loc_forward(ipc_callid_t callid, ipc_call_t *call)
{
	fibril_mutex_lock(&services_list_mutex);
	
	/*
	 * Get ID from request
	 */
	service_id_t id = IPC_GET_ARG2(*call);
	loc_service_t *svc = loc_service_find_id(id);
	
	if ((svc == NULL) || (svc->server == NULL) || (!svc->server->sess)) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOENT);
		return;
	}
	
	async_exch_t *exch = async_exchange_begin(svc->server->sess);
	
	if (svc->forward_interface == 0)
		async_forward_fast(callid, exch, svc->id, 0, 0, IPC_FF_NONE);
	else
		async_forward_fast(callid, exch, svc->forward_interface,
		    svc->id, 0, IPC_FF_NONE);
	
	async_exchange_end(exch);
	
	fibril_mutex_unlock(&services_list_mutex);
}

/** Find ID for service identified by name.
 *
 * In answer will be send EOK and service ID in arg1 or a error
 * code from errno.h.
 *
 */
static void loc_service_get_id(ipc_callid_t iid, ipc_call_t *icall)
{
	char *fqsn;
	
	/* Get fqsn */
	int rc = async_data_write_accept((void **) &fqsn, true, 0,
	    LOC_NAME_MAXLEN, 0, NULL);
	if (rc != EOK) {
		async_answer_0(iid, rc);
		return;
	}
	
	char *ns_name;
	char *name;
	if (!loc_fqsn_split(fqsn, &ns_name, &name)) {
		free(fqsn);
		async_answer_0(iid, EINVAL);
		return;
	}
	
	free(fqsn);
	
	fibril_mutex_lock(&services_list_mutex);
	const loc_service_t *svc;
	
recheck:
	
	/*
	 * Find service name in the list of known services.
	 */
	svc = loc_service_find_name(ns_name, name);
	
	/*
	 * Device was not found.
	 */
	if (svc == NULL) {
		if (IPC_GET_ARG1(*icall) & IPC_FLAG_BLOCKING) {
			/* Blocking lookup */
			fibril_condvar_wait(&services_list_cv,
			    &services_list_mutex);
			goto recheck;
		}
		
		async_answer_0(iid, ENOENT);
		free(ns_name);
		free(name);
		fibril_mutex_unlock(&services_list_mutex);
		return;
	}
	
	async_answer_1(iid, EOK, svc->id);
	
	fibril_mutex_unlock(&services_list_mutex);
	free(ns_name);
	free(name);
}

/** Find ID for namespace identified by name.
 *
 * In answer will be send EOK and service ID in arg1 or a error
 * code from errno.h.
 *
 */
static void loc_namespace_get_id(ipc_callid_t iid, ipc_call_t *icall)
{
	char *name;
	
	/* Get service name */
	int rc = async_data_write_accept((void **) &name, true, 0,
	    LOC_NAME_MAXLEN, 0, NULL);
	if (rc != EOK) {
		async_answer_0(iid, rc);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	const loc_namespace_t *namespace;
	
recheck:
	
	/*
	 * Find namespace name in the list of known namespaces.
	 */
	namespace = loc_namespace_find_name(name);
	
	/*
	 * Namespace was not found.
	 */
	if (namespace == NULL) {
		if (IPC_GET_ARG1(*icall) & IPC_FLAG_BLOCKING) {
			/* Blocking lookup */
			fibril_condvar_wait(&services_list_cv,
			    &services_list_mutex);
			goto recheck;
		}
		
		async_answer_0(iid, ENOENT);
		free(name);
		fibril_mutex_unlock(&services_list_mutex);
		return;
	}
	
	async_answer_1(iid, EOK, namespace->id);
	
	fibril_mutex_unlock(&services_list_mutex);
	free(name);
}

/** Create callback connection.
 *
 * Create callback connection which will be used to send category change
 * events.
 *
 * On success, answer will contain EOK int retval.
 * On failure, error code will be sent in retval.
 *
 */
static void loc_callback_create(ipc_callid_t iid, ipc_call_t *icall)
{
	cb_sess_t *cb_sess = calloc(1, sizeof(cb_sess_t));
	if (cb_sess == NULL) {
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	async_sess_t *sess = async_callback_receive(EXCHANGE_SERIALIZE);
	if (sess == NULL) {
		free(cb_sess);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	cb_sess->sess = sess;
	link_initialize(&cb_sess->cb_sess_list);
	
	fibril_mutex_lock(&callback_sess_mutex);
	list_append(&cb_sess->cb_sess_list, &callback_sess_list);
	fibril_mutex_unlock(&callback_sess_mutex);
	
	async_answer_0(iid, EOK);
}

void loc_category_change_event(void)
{
	fibril_mutex_lock(&callback_sess_mutex);
	
	list_foreach(callback_sess_list, cb_sess_list, cb_sess_t, cb_sess) {
		async_exch_t *exch = async_exchange_begin(cb_sess->sess);
		async_msg_0(exch, LOC_EVENT_CAT_CHANGE);
		async_exchange_end(exch);
	}
	
	fibril_mutex_unlock(&callback_sess_mutex);
}

/** Find ID for category specified by name.
 *
 * On success, answer will contain EOK int retval and service ID in arg1.
 * On failure, error code will be sent in retval.
 *
 */
static void loc_category_get_id(ipc_callid_t iid, ipc_call_t *icall)
{
	char *name;
	category_t *cat;
	
	/* Get service name */
	int rc = async_data_write_accept((void **) &name, true, 0,
	    LOC_NAME_MAXLEN, 0, NULL);
	if (rc != EOK) {
		async_answer_0(iid, rc);
		return;
	}
	
	fibril_mutex_lock(&cdir.mutex);

	cat = category_find_by_name(&cdir, name);
	if (cat == NULL) {
		/* Category not found */
		async_answer_0(iid, ENOENT);
		goto cleanup;
	}
	
	async_answer_1(iid, EOK, cat->id);
cleanup:
	fibril_mutex_unlock(&cdir.mutex);
	free(name);
}

static void loc_id_probe(ipc_callid_t iid, ipc_call_t *icall)
{
	fibril_mutex_lock(&services_list_mutex);
	
	loc_namespace_t *namespace =
	    loc_namespace_find_id(IPC_GET_ARG1(*icall));
	if (namespace == NULL) {
		loc_service_t *svc =
		    loc_service_find_id(IPC_GET_ARG1(*icall));
		if (svc == NULL)
			async_answer_1(iid, EOK, LOC_OBJECT_NONE);
		else
			async_answer_1(iid, EOK, LOC_OBJECT_SERVICE);
	} else
		async_answer_1(iid, EOK, LOC_OBJECT_NAMESPACE);
	
	fibril_mutex_unlock(&services_list_mutex);
}

static void loc_get_namespace_count(ipc_callid_t iid, ipc_call_t *icall)
{
	fibril_mutex_lock(&services_list_mutex);
	async_answer_1(iid, EOK, list_count(&namespaces_list));
	fibril_mutex_unlock(&services_list_mutex);
}

static void loc_get_service_count(ipc_callid_t iid, ipc_call_t *icall)
{
	fibril_mutex_lock(&services_list_mutex);
	
	loc_namespace_t *namespace =
	    loc_namespace_find_id(IPC_GET_ARG1(*icall));
	if (namespace == NULL)
		async_answer_0(iid, EEXISTS);
	else
		async_answer_1(iid, EOK, namespace->refcnt);
	
	fibril_mutex_unlock(&services_list_mutex);
}

static void loc_get_categories(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	size_t act_size;
	int rc;
	
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	category_id_t *id_buf = (category_id_t *) malloc(size);
	if (id_buf == NULL) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, ENOMEM);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	fibril_mutex_lock(&cdir.mutex);
	
	rc = categ_dir_get_categories(&cdir, id_buf, size, &act_size);
	if (rc != EOK) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, rc);
		async_answer_0(iid, rc);
		return;
	}
	
	fibril_mutex_unlock(&cdir.mutex);
	
	sysarg_t retval = async_data_read_finalize(callid, id_buf, size);
	free(id_buf);
	
	async_answer_1(iid, retval, act_size);
}

static void loc_get_namespaces(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	if ((size % sizeof(loc_sdesc_t)) != 0) {
		async_answer_0(callid, EINVAL);
		async_answer_0(iid, EINVAL);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	
	size_t count = size / sizeof(loc_sdesc_t);
	if (count != list_count(&namespaces_list)) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, EOVERFLOW);
		async_answer_0(iid, EOVERFLOW);
		return;
	}
	
	loc_sdesc_t *desc = (loc_sdesc_t *) malloc(size);
	if (desc == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOMEM);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	size_t pos = 0;
	list_foreach(namespaces_list, namespaces, loc_namespace_t, namespace) {
		desc[pos].id = namespace->id;
		str_cpy(desc[pos].name, LOC_NAME_MAXLEN, namespace->name);
		pos++;
	}
	
	sysarg_t retval = async_data_read_finalize(callid, desc, size);
	
	free(desc);
	fibril_mutex_unlock(&services_list_mutex);
	
	async_answer_0(iid, retval);
}

static void loc_get_services(ipc_callid_t iid, ipc_call_t *icall)
{
	/* FIXME: Use faster algorithm which can make better use
	   of namespaces */
	
	ipc_callid_t callid;
	size_t size;
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	if ((size % sizeof(loc_sdesc_t)) != 0) {
		async_answer_0(callid, EINVAL);
		async_answer_0(iid, EINVAL);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	
	loc_namespace_t *namespace =
	    loc_namespace_find_id(IPC_GET_ARG1(*icall));
	if (namespace == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOENT);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	size_t count = size / sizeof(loc_sdesc_t);
	if (count != namespace->refcnt) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, EOVERFLOW);
		async_answer_0(iid, EOVERFLOW);
		return;
	}
	
	loc_sdesc_t *desc = (loc_sdesc_t *) malloc(size);
	if (desc == NULL) {
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(callid, ENOMEM);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	size_t pos = 0;
	list_foreach(services_list, services, loc_service_t, service) {
		if (service->namespace == namespace) {
			desc[pos].id = service->id;
			str_cpy(desc[pos].name, LOC_NAME_MAXLEN, service->name);
			pos++;
		}
	}
	
	sysarg_t retval = async_data_read_finalize(callid, desc, size);
	
	free(desc);
	fibril_mutex_unlock(&services_list_mutex);
	
	async_answer_0(iid, retval);
}

static void loc_category_get_svcs(ipc_callid_t iid, ipc_call_t *icall)
{
	ipc_callid_t callid;
	size_t size;
	size_t act_size;
	int rc;
	
	if (!async_data_read_receive(&callid, &size)) {
		async_answer_0(callid, EREFUSED);
		async_answer_0(iid, EREFUSED);
		return;
	}
	
	fibril_mutex_lock(&cdir.mutex);
	
	category_t *cat = category_get(&cdir, IPC_GET_ARG1(*icall));
	if (cat == NULL) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, ENOENT);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	category_id_t *id_buf = (category_id_t *) malloc(size);
	if (id_buf == NULL) {
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, ENOMEM);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	fibril_mutex_lock(&cat->mutex);
	
	rc = category_get_services(cat, id_buf, size, &act_size);
	if (rc != EOK) {
		fibril_mutex_unlock(&cat->mutex);
		fibril_mutex_unlock(&cdir.mutex);
		async_answer_0(callid, rc);
		async_answer_0(iid, rc);
		return;
	}
	
	fibril_mutex_unlock(&cat->mutex);
	fibril_mutex_unlock(&cdir.mutex);
	
	sysarg_t retval = async_data_read_finalize(callid, id_buf, size);
	free(id_buf);
	
	async_answer_1(iid, retval, act_size);
}


static void loc_null_create(ipc_callid_t iid, ipc_call_t *icall)
{
	fibril_mutex_lock(&null_services_mutex);
	
	unsigned int i;
	bool fnd = false;
	
	for (i = 0; i < NULL_SERVICES; i++) {
		if (null_services[i] == NULL) {
			fnd = true;
			break;
		}
	}
	
	if (!fnd) {
		fibril_mutex_unlock(&null_services_mutex);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	char null[LOC_NAME_MAXLEN];
	snprintf(null, LOC_NAME_MAXLEN, "%u", i);
	
	char *dev_name = str_dup(null);
	if (dev_name == NULL) {
		fibril_mutex_unlock(&null_services_mutex);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	loc_service_t *service =
	    (loc_service_t *) malloc(sizeof(loc_service_t));
	if (service == NULL) {
		fibril_mutex_unlock(&null_services_mutex);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	
	loc_namespace_t *namespace = loc_namespace_create("null");
	if (namespace == NULL) {
		fibril_mutex_lock(&services_list_mutex);
		fibril_mutex_unlock(&null_services_mutex);
		async_answer_0(iid, ENOMEM);
		return;
	}
	
	link_initialize(&service->services);
	link_initialize(&service->server_services);
	list_initialize(&service->cat_memb);
	
	/* Get unique service ID */
	service->id = loc_create_id();
	service->server = NULL;
	
	loc_namespace_addref(namespace, service);
	service->name = dev_name;
	
	/*
	 * Insert service into list of all services and into null services array.
	 * Insert service into a dummy list of null server's services so that it
	 * can be safely removed later.
	 */
	list_append(&service->services, &services_list);
	list_append(&service->server_services, &dummy_null_services);
	null_services[i] = service;
	
	fibril_mutex_unlock(&services_list_mutex);
	fibril_mutex_unlock(&null_services_mutex);
	
	async_answer_1(iid, EOK, (sysarg_t) i);
}

static void loc_null_destroy(ipc_callid_t iid, ipc_call_t *icall)
{
	sysarg_t i = IPC_GET_ARG1(*icall);
	if (i >= NULL_SERVICES) {
		async_answer_0(iid, ELIMIT);
		return;
	}
	
	fibril_mutex_lock(&null_services_mutex);
	
	if (null_services[i] == NULL) {
		fibril_mutex_unlock(&null_services_mutex);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	fibril_mutex_lock(&services_list_mutex);
	fibril_mutex_lock(&cdir.mutex);
	loc_service_unregister_core(null_services[i]);
	fibril_mutex_unlock(&cdir.mutex);
	fibril_mutex_unlock(&services_list_mutex);
	
	null_services[i] = NULL;
	
	fibril_mutex_unlock(&null_services_mutex);
	async_answer_0(iid, EOK);
}

static void loc_service_add_to_cat(ipc_callid_t iid, ipc_call_t *icall)
{
	category_t *cat;
	loc_service_t *svc;
	catid_t cat_id;
	service_id_t svc_id;
	sysarg_t retval;
	
	svc_id = IPC_GET_ARG1(*icall);
	cat_id = IPC_GET_ARG2(*icall);
	
	fibril_mutex_lock(&services_list_mutex);
	fibril_mutex_lock(&cdir.mutex);
	
	cat = category_get(&cdir, cat_id);
	svc = loc_service_find_id(svc_id);
	
	if (cat == NULL || svc == NULL) {
		fibril_mutex_unlock(&cdir.mutex);
		fibril_mutex_unlock(&services_list_mutex);
		async_answer_0(iid, ENOENT);
		return;
	}
	
	fibril_mutex_lock(&cat->mutex);
	retval = category_add_service(cat, svc);

	fibril_mutex_unlock(&cat->mutex);
	fibril_mutex_unlock(&cdir.mutex);
	fibril_mutex_unlock(&services_list_mutex);

	async_answer_0(iid, retval);

	loc_category_change_event();
}


/** Initialize location service.
 *
 *
 */
static bool loc_init(void)
{
	unsigned int i;
	category_t *cat;

	for (i = 0; i < NULL_SERVICES; i++)
		null_services[i] = NULL;
	
	categ_dir_init(&cdir);

	cat = category_new("bd");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("iplink");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("keyboard");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("mouse");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("serial");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("clock");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("test3");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("usbhc");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("virt-null");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("virtual");
	categ_dir_add_cat(&cdir, cat);
	
	cat = category_new("nic");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("visualizer");
	categ_dir_add_cat(&cdir, cat);

	cat = category_new("renderer");
	categ_dir_add_cat(&cdir, cat);
	
	cat = category_new("audio-pcm");
	categ_dir_add_cat(&cdir, cat);
	
	return true;
}

/** Handle connection on supplier port.
 *
 */
static void loc_connection_supplier(ipc_callid_t iid, ipc_call_t *icall)
{
	/* Accept connection */
	async_answer_0(iid, EOK);
	
	loc_server_t *server = loc_server_register();
	if (server == NULL)
		return;
	
	while (true) {
		ipc_call_t call;
		ipc_callid_t callid = async_get_call(&call);
		
		if (!IPC_GET_IMETHOD(call))
			break;
		
		switch (IPC_GET_IMETHOD(call)) {
		case LOC_SERVER_UNREGISTER:
			if (server == NULL)
				async_answer_0(callid, ENOENT);
			else
				async_answer_0(callid, EOK);
			break;
		case LOC_SERVICE_ADD_TO_CAT:
			/* Add service to category */
			loc_service_add_to_cat(callid, &call);
			break;
		case LOC_SERVICE_REGISTER:
			/* Register one service */
			loc_service_register(callid, &call, server);
			break;
		case LOC_SERVICE_UNREGISTER:
			/* Remove one service */
			loc_service_unregister(callid, &call, server);
			break;
		case LOC_SERVICE_GET_ID:
			loc_service_get_id(callid, &call);
			break;
		case LOC_NAMESPACE_GET_ID:
			loc_namespace_get_id(callid, &call);
			break;
		default:
			async_answer_0(callid, ENOENT);
		}
	}
	
	if (server != NULL) {
		/*
		 * Unregister the server and all its services.
		 */
		loc_server_unregister(server);
		server = NULL;
	}
}

/** Handle connection on consumer port.
 *
 */
static void loc_connection_consumer(ipc_callid_t iid, ipc_call_t *icall)
{
	/* Accept connection */
	async_answer_0(iid, EOK);
	
	while (true) {
		ipc_call_t call;
		ipc_callid_t callid = async_get_call(&call);
		
		if (!IPC_GET_IMETHOD(call))
			break;
		
		switch (IPC_GET_IMETHOD(call)) {
		case LOC_SERVICE_GET_ID:
			loc_service_get_id(callid, &call);
			break;
		case LOC_SERVICE_GET_NAME:
			loc_service_get_name(callid, &call);
			break;
		case LOC_SERVICE_GET_SERVER_NAME:
			loc_service_get_server_name(callid, &call);
			break;
		case LOC_NAMESPACE_GET_ID:
			loc_namespace_get_id(callid, &call);
			break;
		case LOC_CALLBACK_CREATE:
			loc_callback_create(callid, &call);
			break;
		case LOC_CATEGORY_GET_ID:
			loc_category_get_id(callid, &call);
			break;
		case LOC_CATEGORY_GET_NAME:
			loc_category_get_name(callid, &call);
			break;
		case LOC_CATEGORY_GET_SVCS:
			loc_category_get_svcs(callid, &call);
			break;
		case LOC_ID_PROBE:
			loc_id_probe(callid, &call);
			break;
		case LOC_NULL_CREATE:
			loc_null_create(callid, &call);
			break;
		case LOC_NULL_DESTROY:
			loc_null_destroy(callid, &call);
			break;
		case LOC_GET_NAMESPACE_COUNT:
			loc_get_namespace_count(callid, &call);
			break;
		case LOC_GET_SERVICE_COUNT:
			loc_get_service_count(callid, &call);
			break;
		case LOC_GET_CATEGORIES:
			loc_get_categories(callid, &call);
			break;
		case LOC_GET_NAMESPACES:
			loc_get_namespaces(callid, &call);
			break;
		case LOC_GET_SERVICES:
			loc_get_services(callid, &call);
			break;
		default:
			async_answer_0(callid, ENOENT);
		}
	}
}

/** Function for handling connections to location service
 *
 */
static void loc_connection(ipc_callid_t iid, ipc_call_t *icall, void *arg)
{
	/* Select interface */
	switch ((sysarg_t) (IPC_GET_ARG1(*icall))) {
	case LOC_PORT_SUPPLIER:
		loc_connection_supplier(iid, icall);
		break;
	case LOC_PORT_CONSUMER:
		loc_connection_consumer(iid, icall);
		break;
	case LOC_CONNECT_TO_SERVICE:
		/* Connect client to selected service */
		loc_forward(iid, icall);
		break;
	default:
		/* No such interface */
		async_answer_0(iid, ENOENT);
	}
}

/**
 *
 */
int main(int argc, char *argv[])
{
	printf("%s: HelenOS Location Service\n", NAME);
	
	if (!loc_init()) {
		printf("%s: Error while initializing service\n", NAME);
		return -1;
	}
	
	/* Set a handler of incomming connections */
	async_set_client_connection(loc_connection);
	
	/* Register location service at naming service */
	int rc = service_register(SERVICE_LOC);
	if (rc != EOK)
		return rc;
	
	printf("%s: Accepting connections\n", NAME);
	async_manager();
	
	/* Never reached */
	return 0;
}

/**
 * @}
 */
