/* PipeWire
 * Copyright (C) 2015 Wim Taymans <wim.taymans@gmail.com>
 *
 * This 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.
 *
 * This 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 this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <unistd.h>
#include <limits.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <pwd.h>
#include <errno.h>
#include <dlfcn.h>

#include <spa/support/dbus.h>

#include "pipewire.h"
#include "private.h"
#include "version.h"

static char **categories = NULL;

static struct support_info {
	void *hnd;
	spa_handle_factory_enum_func_t enum_func;
	struct spa_support support[16];
	uint32_t n_support;
} support_info;

struct interface {
	struct spa_list link;
	struct spa_handle *handle;
	uint32_t type;
	void *iface;
};

struct registry {
	struct spa_list interfaces;
};

static struct registry global_registry;

static bool
open_support(const char *path,
	     const char *lib,
	     struct support_info *info)
{
	char *filename;

        if (asprintf(&filename, "%s/%s.so", path, lib) < 0)
		goto no_filename;

        if ((info->hnd = dlopen(filename, RTLD_NOW)) == NULL) {
                fprintf(stderr, "can't load %s: %s\n", filename, dlerror());
                goto open_failed;
        }
        if ((info->enum_func = dlsym(info->hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
                fprintf(stderr, "can't find enum function in %s\n", filename);
                goto no_symbol;
        }
	free(filename);
	return true;

      no_filename:
	return false;
      no_symbol:
	dlclose(info->hnd);
      open_failed:
        free(filename);
	return false;
}

static const struct spa_handle_factory *get_factory(struct support_info *info, const char *factory_name)
{
	int res;
	uint32_t index;
        const struct spa_handle_factory *factory;

        for (index = 0;;) {
                if ((res = info->enum_func(&factory, &index)) <= 0) {
                        if (res != 0)
                                fprintf(stderr, "can't enumerate factories: %s\n", spa_strerror(res));
                        break;
                }
                if (strcmp(factory->name, factory_name) == 0)
                        return factory;
	}
	return NULL;
}

static struct interface *
load_interface(struct support_info *info,
	       const char *factory_name,
	       const char *type)
{
        int res;
        struct spa_handle *handle;
        uint32_t type_id;
        const struct spa_handle_factory *factory;
	struct interface *iface;
        void *ptr;
	struct spa_type_map *map = NULL;

	factory = get_factory(info, factory_name);
	if (factory == NULL)
		goto not_found;

        handle = calloc(1, factory->size);
        if ((res = spa_handle_factory_init(factory,
                                           handle, NULL, info->support, info->n_support)) < 0) {
                fprintf(stderr, "can't make factory instance: %d\n", res);
                goto init_failed;
        }

	map = pw_get_support_interface(SPA_TYPE__TypeMap);
	type_id = map ? spa_type_map_get_id(map, type) : 0;

        if ((res = spa_handle_get_interface(handle, type_id, &ptr)) < 0) {
                fprintf(stderr, "can't get %s interface %d\n", type, res);
                goto interface_failed;
        }

	if ((iface = calloc(1, sizeof(struct interface))) == NULL)
		goto alloc_failed;

	iface->handle = handle;
	iface->type = type_id;
	iface->iface = ptr;
	spa_list_append(&global_registry.interfaces, &iface->link);

        return iface;

      alloc_failed:
      interface_failed:
	spa_handle_clear(handle);
      init_failed:
	free(handle);
      not_found:
	return NULL;
}

static void configure_debug(const char *str)
{
	char **level;
	int n_tokens;

	level = pw_split_strv(str, ":", INT_MAX, &n_tokens);
	if (n_tokens > 0)
		pw_log_set_level(atoi(level[0]));

	if (n_tokens > 1)
		categories = pw_split_strv(level[1], ",", INT_MAX, &n_tokens);
}

/** Get a support interface
 * \param type the interface type
 * \return the interface or NULL when not configured
 */
SPA_EXPORT
void *pw_get_support_interface(const char *type)
{
	int i;

	for (i = 0; i < support_info.n_support; i++) {
		if (strcmp(support_info.support->type, type) == 0)
			return support_info.support->data;
	}
	return NULL;
}

SPA_EXPORT
const struct spa_handle_factory *pw_get_support_factory(const char *factory_name)
{
	return get_factory(&support_info, factory_name);
}

SPA_EXPORT
const struct spa_support *pw_get_support(uint32_t *n_support)
{
	*n_support = support_info.n_support;
	return support_info.support;
}

SPA_EXPORT
void *pw_get_spa_dbus(struct pw_loop *loop)
{
	struct support_info dbus_support_info;
	const char *str;
	struct interface *iface;

	dbus_support_info.n_support = support_info.n_support;
	memcpy(dbus_support_info.support, support_info.support,
			sizeof(struct spa_support) * dbus_support_info.n_support);

	dbus_support_info.support[dbus_support_info.n_support++] =
			SPA_SUPPORT_INIT(SPA_TYPE__LoopUtils, loop->utils);

	if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
		str = PLUGINDIR;

	if (open_support(str, "support/libspa-dbus", &dbus_support_info)) {
		iface = load_interface(&dbus_support_info, "dbus", SPA_TYPE__DBus);
		if (iface != NULL)
			return iface->iface;
	}
	return NULL;
}
static struct interface *find_interface(void *iface)
{
	struct interface *i;
	spa_list_for_each(i, &global_registry.interfaces, link) {
		if (i->iface == iface)
			return i;
	}
	return NULL;
}

SPA_EXPORT
int pw_release_spa_dbus(void *dbus)
{
	struct interface *iface;

	if ((iface = find_interface(dbus)) == NULL)
		return -ENOENT;

	spa_list_remove(&iface->link);
	spa_handle_clear(iface->handle);
	free(iface->handle);
	free(iface);
	return 0;
}

/** Initialize PipeWire
 *
 * \param argc pointer to argc
 * \param argv pointer to argv
 *
 * Initialize the PipeWire system, parse and modify any parameters given
 * by \a argc and \a argv and set up debugging.
 *
 * The environment variable \a PIPEWIRE_DEBUG
 *
 * \memberof pw_pipewire
 */
SPA_EXPORT
void pw_init(int *argc, char **argv[])
{
	const char *str;
	struct interface *iface;
	struct support_info *info = &support_info;

	if ((str = getenv("PIPEWIRE_DEBUG")))
		configure_debug(str);

	if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
		str = PLUGINDIR;

	if (support_info.n_support > 0)
		return;

	spa_list_init(&global_registry.interfaces);

	if (open_support(str, "support/libspa-support", info)) {
		iface = load_interface(info, "mapper", SPA_TYPE__TypeMap);
		if (iface != NULL)
			info->support[info->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE__TypeMap, iface->iface);

		iface = load_interface(info, "logger", SPA_TYPE__Log);
		if (iface != NULL) {
			info->support[info->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE__Log, iface->iface);
			pw_log_set(iface->iface);
		}
	}
	pw_log_info("version %s", pw_get_library_version());
}

/** Check if a debug category is enabled
 *
 * \param name the name of the category to check
 * \return true if enabled
 *
 * Debugging categories can be enabled by using the PIPEWIRE_DEBUG
 * environment variable
 *
 * \memberof pw_pipewire
 */
SPA_EXPORT
bool pw_debug_is_category_enabled(const char *name)
{
	int i;

	if (categories == NULL)
		return false;

	for (i = 0; categories[i]; i++) {
		if (strcmp (categories[i], name) == 0)
			return true;
	}
	return false;
}

/** Get the application name \memberof pw_pipewire */
SPA_EXPORT
const char *pw_get_application_name(void)
{
	return NULL;
}

/** Get the program name \memberof pw_pipewire */
SPA_EXPORT
const char *pw_get_prgname(void)
{
	static char tcomm[16 + 1];
	spa_zero(tcomm);

	if (prctl(PR_GET_NAME, (unsigned long) tcomm, 0, 0, 0) == 0)
		return tcomm;

	return NULL;
}

/** Get the user name \memberof pw_pipewire */
SPA_EXPORT
const char *pw_get_user_name(void)
{
	struct passwd *pw;

	if ((pw = getpwuid(getuid())))
		return pw->pw_name;

	return NULL;
}

/** Get the host name \memberof pw_pipewire */
SPA_EXPORT
const char *pw_get_host_name(void)
{
	static char hname[256];

	if (gethostname(hname, 256) < 0)
		return NULL;

	hname[255] = 0;
	return hname;
}

/** Get the client name
 *
 * Make a new PipeWire client name that can be used to construct a remote.
 *
 * \memberof pw_pipewire
 */
SPA_EXPORT
char *pw_get_client_name(void)
{
	char *c;
	const char *cc;

	if ((cc = pw_get_application_name()))
		return strdup(cc);
	else if ((cc = pw_get_prgname()))
		return strdup(cc);
	else {
		if (asprintf(&c, "pipewire-pid-%zd", (size_t) getpid()) < 0)
			return NULL;
		return c;
	}
}

/** Fill remote properties
 * \param properties a \ref pw_properties
 *
 * Fill \a properties with a set of default remote properties.
 *
 * \memberof pw_pipewire
 */
SPA_EXPORT
void pw_fill_remote_properties(struct pw_core *core, struct pw_properties *properties)
{
	const char *val;

	if (!pw_properties_get(properties, "application.name"))
		pw_properties_set(properties, "application.name", pw_get_application_name());

	if (!pw_properties_get(properties, "application.prgname"))
		pw_properties_set(properties, "application.prgname", pw_get_prgname());

	if (!pw_properties_get(properties, "application.language")) {
		pw_properties_set(properties, "application.language", getenv("LANG"));
	}
	if (!pw_properties_get(properties, "application.process.id")) {
		pw_properties_setf(properties, "application.process.id", "%zd", (size_t) getpid());
	}
	if (!pw_properties_get(properties, "application.process.user"))
		pw_properties_set(properties, "application.process.user", pw_get_user_name());

	if (!pw_properties_get(properties, "application.process.host"))
		pw_properties_set(properties, "application.process.host", pw_get_host_name());

	if (!pw_properties_get(properties, "application.process.session_id")) {
		pw_properties_set(properties, "application.process.session_id",
				  getenv("XDG_SESSION_ID"));
	}
	pw_properties_set(properties, PW_CORE_PROP_VERSION, core->info.version);
	pw_properties_set(properties, PW_CORE_PROP_NAME, core->info.name);

	if ((val = pw_properties_get(core->properties, PW_CORE_PROP_DAEMON)))
		pw_properties_set(properties, PW_CORE_PROP_DAEMON, val);
}

/** Fill stream properties
 * \param properties a \ref pw_properties
 *
 * Fill \a properties with a set of default stream properties.
 *
 * \memberof pw_pipewire
 */
SPA_EXPORT
void pw_fill_stream_properties(struct pw_core *core, struct pw_properties *properties)
{
}

/** Reverse the direction \memberof pw_pipewire */
enum pw_direction pw_direction_reverse(enum pw_direction direction)
{
	if (direction == PW_DIRECTION_INPUT)
		return PW_DIRECTION_OUTPUT;
	else if (direction == PW_DIRECTION_OUTPUT)
		return PW_DIRECTION_INPUT;
	return direction;
}

/** Get the currently running version */
SPA_EXPORT
const char* pw_get_library_version(void)
{
	return pw_get_headers_version();
}
