/*
 * Copyright (c) 2012 Jan Vesely
 * 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.
 */

/**
 * @addtogroup audio
 * @brief HelenOS sound server.
 * @{
 */
/** @file
 */

#include <assert.h>
#include <stdlib.h>
#include <str.h>

#include "hound.h"
#include "audio_device.h"
#include "audio_sink.h"
#include "audio_source.h"
#include "connection.h"
#include "log.h"
#include "errno.h"
#include "str_error.h"

/**
 * Search devices by name.
 * @param name String identifier.
 * @return Pointer to the found device, NULL on failure.
 */
static audio_device_t *find_device_by_name(list_t *list, const char *name)
{
	assert(list);
	assert(name);

	list_foreach(*list, link, audio_device_t, dev) {
		if (str_cmp(name, dev->name) == 0) {
			log_debug("device with name '%s' is in the list",
			    name);
			return dev;
		}
	}

	return NULL;
}

/**
 * Search sources by name.
 * @param name String identifier.
 * @return Pointer to the found source, NULL on failure.
 */
static audio_source_t *find_source_by_name(list_t *list, const char *name)
{
	assert(list);
	assert(name);

	list_foreach(*list, link, audio_source_t, dev) {
		if (str_cmp(name, dev->name) == 0) {
			log_debug("source with name '%s' is in the list",
			    name);
			return dev;
		}
	}

	return NULL;
}

/**
 * Search sinks by name.
 * @param name String identifier.
 * @return Pointer to the found sink, NULL on failure.
 */
static audio_sink_t *find_sink_by_name(list_t *list, const char *name)
{
	assert(list);
	assert(name);

	list_foreach(*list, link, audio_sink_t, dev) {
		if (str_cmp(name, dev->name) == 0) {
			log_debug("sink with name '%s' is in the list",
			    name);
			return dev;
		}
	}

	return NULL;
}

static errno_t hound_disconnect_internal(hound_t *hound, const char *source_name, const char *sink_name);

/**
 * Remove provided sink.
 * @param hound The hound structure.
 * @param sink Target sink to remove.
 *
 * This function has to be called with the list_guard lock held.
 */
static void hound_remove_sink_internal(hound_t *hound, audio_sink_t *sink)
{
	assert(hound);
	assert(sink);
	assert(fibril_mutex_is_locked(&hound->list_guard));
	log_verbose("Removing sink '%s'.", sink->name);
	if (!list_empty(&sink->connections))
		log_warning("Removing sink '%s' while still connected.", sink->name);
	while (!list_empty(&sink->connections)) {
		connection_t *conn = list_get_instance(
		    list_first(&sink->connections), connection_t, sink_link);
		list_remove(&conn->hound_link);
		connection_destroy(conn);
	}
	list_remove(&sink->link);
}

/**
 * Remove provided source.
 * @param hound The hound structure.
 * @param sink Target source to remove.
 *
 * This function has to be called with the guard lock held.
 */
static void hound_remove_source_internal(hound_t *hound, audio_source_t *source)
{
	assert(fibril_mutex_is_locked(&hound->list_guard));
	log_verbose("Removing source '%s'.", source->name);
	if (!list_empty(&source->connections))
		log_warning("Removing source '%s' while still connected.", source->name);
	while (!list_empty(&source->connections)) {
		connection_t *conn = list_get_instance(
		    list_first(&source->connections), connection_t, source_link);
		list_remove(&conn->hound_link);
		connection_destroy(conn);
	}
	list_remove(&source->link);
}

/**
 * Initialize hound structure.
 * @param hound The structure to initialize.
 * @return Error code.
 */
errno_t hound_init(hound_t *hound)
{
	assert(hound);
	fibril_mutex_initialize(&hound->list_guard);
	list_initialize(&hound->devices);
	list_initialize(&hound->contexts);
	list_initialize(&hound->sources);
	list_initialize(&hound->sinks);
	list_initialize(&hound->connections);
	return EOK;
}

/**
 * Add a new application context.
 * @param hound Hound structure.
 * @param ctx Context to add.
 * @return Error code.
 */
errno_t hound_add_ctx(hound_t *hound, hound_ctx_t *ctx)
{
	log_info("Trying to add context %p", ctx);
	assert(hound);
	if (!ctx)
		return EINVAL;
	fibril_mutex_lock(&hound->list_guard);
	list_append(&ctx->link, &hound->contexts);
	fibril_mutex_unlock(&hound->list_guard);
	errno_t ret = EOK;
	if (ret == EOK && ctx->source)
		ret = hound_add_source(hound, ctx->source);
	if (ret == EOK && ctx->sink)
		ret = hound_add_sink(hound, ctx->sink);
	if (ret != EOK) {
		fibril_mutex_lock(&hound->list_guard);
		list_remove(&ctx->link);
		fibril_mutex_unlock(&hound->list_guard);
	}
	return ret;
}

/**
 * Remove existing application context.
 * @param hound Hound structure.
 * @param ctx Context to remove.
 * @return Error code.
 */
errno_t hound_remove_ctx(hound_t *hound, hound_ctx_t *ctx)
{
	assert(hound);
	if (!ctx)
		return EINVAL;
	if (!list_empty(&ctx->streams))
		return EBUSY;
	fibril_mutex_lock(&hound->list_guard);
	list_remove(&ctx->link);
	if (ctx->source)
		hound_remove_source_internal(hound, ctx->source);
	if (ctx->sink)
		hound_remove_sink_internal(hound, ctx->sink);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * Search registered contexts for the matching id.
 * @param hound The hound structure.
 * @param id Requested id.
 * @return Pointer to the found structure, NULL on failure.
 */
hound_ctx_t *hound_get_ctx_by_id(hound_t *hound, hound_context_id_t id)
{
	assert(hound);

	fibril_mutex_lock(&hound->list_guard);
	hound_ctx_t *res = NULL;
	list_foreach(hound->contexts, link, hound_ctx_t, ctx) {
		if (hound_ctx_get_id(ctx) == id) {
			res = ctx;
			break;
		}
	}
	fibril_mutex_unlock(&hound->list_guard);
	return res;
}

/**
 * Add a new device.
 * @param hound The hound structure.
 * @param id Locations service id representing the device driver.
 * @param name String identifier.
 * @return Error code.
 */
errno_t hound_add_device(hound_t *hound, service_id_t id, const char *name)
{
	log_verbose("Adding device \"%s\", service: %zu", name, id);

	assert(hound);
	if (!name || !id) {
		log_debug("Incorrect parameters.");
		return EINVAL;
	}

	list_foreach(hound->devices, link, audio_device_t, dev) {
		if (dev->id == id) {
			log_debug("Device with id %zu is already present", id);
			return EEXIST;
		}
	}

	audio_device_t *dev = find_device_by_name(&hound->devices, name);
	if (dev) {
		log_debug("Device with name %s is already present", name);
		return EEXIST;
	}

	dev = malloc(sizeof(audio_device_t));
	if (!dev) {
		log_debug("Failed to malloc device structure.");
		return ENOMEM;
	}

	const errno_t ret = audio_device_init(dev, id, name);
	if (ret != EOK) {
		log_debug("Failed to initialize new audio device: %s",
		    str_error(ret));
		free(dev);
		return ret;
	}

	list_append(&dev->link, &hound->devices);
	log_info("Added new device: '%s'", dev->name);

	audio_source_t *source = audio_device_get_source(dev);
	if (source) {
		const errno_t ret = hound_add_source(hound, source);
		if (ret != EOK) {
			log_debug("Failed to add device source: %s",
			    str_error(ret));
			audio_device_fini(dev);
			return ret;
		}
		log_verbose("Added source: '%s'.", source->name);
	}

	audio_sink_t *sink = audio_device_get_sink(dev);
	if (sink) {
		const errno_t ret = hound_add_sink(hound, sink);
		if (ret != EOK) {
			log_debug("Failed to add device sink: %s",
			    str_error(ret));
			audio_device_fini(dev);
			return ret;
		}
		log_verbose("Added sink: '%s'.", sink->name);
	}

	if (!source && !sink)
		log_warning("Neither sink nor source on device '%s'.", name);

	return ret;
}

/**
 * Register a new source.
 * @param hound The hound structure.
 * @param source A new source to add.
 * @return Error code.
 */
errno_t hound_add_source(hound_t *hound, audio_source_t *source)
{
	assert(hound);
	if (!source || !source->name || str_cmp(source->name, "default") == 0) {
		log_debug("Invalid source specified.");
		return EINVAL;
	}
	fibril_mutex_lock(&hound->list_guard);
	if (find_source_by_name(&hound->sources, source->name)) {
		log_debug("Source by that name already exists");
		fibril_mutex_unlock(&hound->list_guard);
		return EEXIST;
	}
	list_append(&source->link, &hound->sources);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * Register a new sink.
 * @param hound The hound structure.
 * @param sink A new sink to add.
 * @return Error code.
 */
errno_t hound_add_sink(hound_t *hound, audio_sink_t *sink)
{
	assert(hound);
	if (!sink || !sink->name || str_cmp(sink->name, "default") == 0) {
		log_debug("Invalid source specified.");
		return EINVAL;
	}
	fibril_mutex_lock(&hound->list_guard);
	if (find_sink_by_name(&hound->sinks, sink->name)) {
		log_debug("Sink by that name already exists");
		fibril_mutex_unlock(&hound->list_guard);
		return EEXIST;
	}
	list_append(&sink->link, &hound->sinks);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * Remove a registered source.
 * @param hound The hound structure.
 * @param source A registered source to remove.
 * @return Error code.
 */
errno_t hound_remove_source(hound_t *hound, audio_source_t *source)
{
	assert(hound);
	if (!source)
		return EINVAL;
	fibril_mutex_lock(&hound->list_guard);
	hound_remove_source_internal(hound, source);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * Remove a registered sink.
 * @param hound The hound structure.
 * @param sink A registered sink to remove.
 * @return Error code.
 */
errno_t hound_remove_sink(hound_t *hound, audio_sink_t *sink)
{
	assert(hound);
	if (!sink)
		return EINVAL;
	fibril_mutex_lock(&hound->list_guard);
	hound_remove_sink_internal(hound, sink);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * List all registered sources.
 * @param[in] hound The hound structure.
 * @param[out] list List of the string identifiers.
 * @param[out] size Number of identifiers int he @p list.
 * @return Error code.
 */
errno_t hound_list_sources(hound_t *hound, char ***list, size_t *size)
{
	assert(hound);
	if (!list || !size)
		return EINVAL;

	fibril_mutex_lock(&hound->list_guard);
	const unsigned long count = list_count(&hound->sources);
	if (count == 0) {
		*list = NULL;
		*size = 0;
		fibril_mutex_unlock(&hound->list_guard);
		return EOK;
	}
	char **names = calloc(count, sizeof(char *));
	errno_t ret = names ? EOK : ENOMEM;
	for (unsigned long i = 0; i < count && ret == EOK; ++i) {
		link_t *slink = list_nth(&hound->sources, i);
		audio_source_t *source = audio_source_list_instance(slink);
		names[i] = str_dup(source->name);
		if (names[i])
			ret = ENOMEM;
	}
	if (ret == EOK) {
		*size = count;
		*list = names;
	} else {
		for (size_t i = 0; i < count; ++i)
			free(names[i]);
		free(names);
	}
	fibril_mutex_unlock(&hound->list_guard);
	return ret;
}

/**
 * List all registered sinks.
 * @param[in] hound The hound structure.
 * @param[out] list List of the string identifiers.
 * @param[out] size Number of identifiers int he @p list.
 * @return Error code.
 */
errno_t hound_list_sinks(hound_t *hound, char ***list, size_t *size)
{
	assert(hound);
	if (!list || !size)
		return EINVAL;

	fibril_mutex_lock(&hound->list_guard);
	const size_t count = list_count(&hound->sinks);
	if (count == 0) {
		*list = NULL;
		*size = 0;
		fibril_mutex_unlock(&hound->list_guard);
		return EOK;
	}
	char **names = calloc(count, sizeof(char *));
	errno_t ret = names ? EOK : ENOMEM;
	for (size_t i = 0; i < count && ret == EOK; ++i) {
		link_t *slink = list_nth(&hound->sinks, i);
		audio_sink_t *sink = audio_sink_list_instance(slink);
		names[i] = str_dup(sink->name);
		if (!names[i])
			ret = ENOMEM;
	}
	if (ret == EOK) {
		*size = count;
		*list = names;
	} else {
		for (size_t i = 0; i < count; ++i)
			free(names[i]);
		free(names);
	}
	fibril_mutex_unlock(&hound->list_guard);
	return ret;
}

/**
 * List all connections
 * @param[in] hound The hound structure.
 * @param[out] sources List of the source string identifiers.
 * @param[out] sinks List of the sinks string identifiers.
 * @param[out] size Number of identifiers int he @p list.
 * @return Error code.
 *
 * Lists include duplicit name entries. The order of entries is important,
 * identifiers with the same index are connected.
 */
errno_t hound_list_connections(hound_t *hound, const char ***sources,
    const char ***sinks, size_t *size)
{
	fibril_mutex_lock(&hound->list_guard);
	fibril_mutex_unlock(&hound->list_guard);
	return ENOTSUP;
}

/**
 * Create and register a new connection.
 * @param hound The hound structure.
 * @param source_name Source's string id.
 * @param sink_name Sink's string id.
 * @return Error code.
 */
errno_t hound_connect(hound_t *hound, const char *source_name, const char *sink_name)
{
	assert(hound);
	log_verbose("Connecting '%s' to '%s'.", source_name, sink_name);
	fibril_mutex_lock(&hound->list_guard);

	audio_source_t *source =
	    audio_source_list_instance(list_first(&hound->sources));
	if (str_cmp(source_name, "default") != 0)
		source = find_source_by_name(&hound->sources, source_name);

	audio_sink_t *sink =
	    audio_sink_list_instance(list_first(&hound->sinks));
	if (str_cmp(sink_name, "default") != 0)
		sink = find_sink_by_name(&hound->sinks, sink_name);

	if (!source || !sink) {
		fibril_mutex_unlock(&hound->list_guard);
		log_debug("Source (%p), or sink (%p) not found", source, sink);
		return ENOENT;
	}
	connection_t *conn = connection_create(source, sink);
	if (!conn) {
		fibril_mutex_unlock(&hound->list_guard);
		log_debug("Failed to create connection");
		return ENOMEM;
	}
	list_append(&conn->hound_link, &hound->connections);
	fibril_mutex_unlock(&hound->list_guard);
	return EOK;
}

/**
 * Find and destroy connection between source and sink.
 * @param hound The hound structure.
 * @param source_name Source's string id.
 * @param sink_name Sink's string id.
 * @return Error code.
 */
errno_t hound_disconnect(hound_t *hound, const char *source_name, const char *sink_name)
{
	assert(hound);
	fibril_mutex_lock(&hound->list_guard);
	const errno_t ret = hound_disconnect_internal(hound, source_name, sink_name);
	fibril_mutex_unlock(&hound->list_guard);
	return ret;
}

/**
 * Internal disconnect helper.
 * @param hound The hound structure.
 * @param source_name Source's string id.
 * @param sink_name Sink's string id.
 * @return Error code.
 *
 * This function has to be called with the list_guard lock held.
 */
static errno_t hound_disconnect_internal(hound_t *hound, const char *source_name,
    const char *sink_name)
{
	assert(hound);
	assert(fibril_mutex_is_locked(&hound->list_guard));
	log_debug("Disconnecting '%s' to '%s'.", source_name, sink_name);

	list_foreach_safe(hound->connections, it, next) {
		connection_t *conn = connection_from_hound_list(it);
		if (str_cmp(connection_source_name(conn), source_name) == 0 ||
		    str_cmp(connection_sink_name(conn), sink_name) == 0) {
			log_debug("Removing %s -> %s", connection_source_name(conn),
			    connection_sink_name(conn));
			list_remove(it);
			connection_destroy(conn);
		}
	}

	return EOK;
}
/**
 * @}
 */
