/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2013  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <string.h>

#include <gdbus.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/notifier.h>
#include <connman/dbus.h>
#include <connman/log.h>
#include <connman/proxy.h>

#define PACRUNNER_SERVICE	"org.pacrunner"
#define PACRUNNER_INTERFACE	"org.pacrunner.Manager"
#define PACRUNNER_PATH		"/org/pacrunner/manager"

#define PACRUNNER_CLIENT_INTERFACE	"org.pacrunner.Client"
#define PACRUNNER_CLIENT_PATH		"/org/pacrunner/client"

#define DBUS_TIMEOUT	5000

struct proxy_data {
	struct connman_service *service;
	char *url;
};

static DBusConnection *connection;
static dbus_bool_t daemon_running = FALSE;

static struct connman_service *default_service = NULL;
static char *current_config = NULL;

static void create_config_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply = dbus_pending_call_steal_reply(call);
	const char *path;

	DBG("");

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		connman_error("Failed to create proxy configuration");
		goto done;
	}

	if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH,
					&path, DBUS_TYPE_INVALID))
		goto done;

	g_free(current_config);
	current_config = g_strdup(path);

done:
	dbus_message_unref(reply);
}

static void append_string_list(DBusMessageIter *iter, void *user_data)
{
	char **list = user_data;
	int i;

	for (i = 0; list[i]; i++)
		dbus_message_iter_append_basic(iter,
					DBUS_TYPE_STRING, &list[i]);
}

static void create_proxy_configuration(void)
{
	DBusMessage *msg;
	DBusMessageIter iter, dict;
	DBusPendingCall *call;
	dbus_bool_t result;
	char *interface;
	const char *method;
	const char *str;
	char **str_list;

	if (!default_service)
		return;

	DBG("");

	msg = dbus_message_new_method_call(PACRUNNER_SERVICE, PACRUNNER_PATH,
			PACRUNNER_INTERFACE, "CreateProxyConfiguration");
	if (!msg)
		return;

	dbus_message_set_auto_start(msg, FALSE);

	dbus_message_iter_init_append(msg, &iter);
	connman_dbus_dict_open(&iter, &dict);

	switch(connman_service_get_proxy_method(default_service)) {
	case CONNMAN_SERVICE_PROXY_METHOD_UNKNOWN:
		connman_dbus_dict_close(&iter, &dict);
		goto done;
	case CONNMAN_SERVICE_PROXY_METHOD_DIRECT:
		method= "direct";
		break;
	case CONNMAN_SERVICE_PROXY_METHOD_MANUAL:
		method = "manual";

		str_list = connman_service_get_proxy_servers(default_service);
		if (!str_list) {
			connman_dbus_dict_close(&iter, &dict);
			goto done;
		}

		connman_dbus_dict_append_array(&dict, "Servers",
					DBUS_TYPE_STRING, append_string_list,
					str_list);
		g_strfreev(str_list);

		str_list = connman_service_get_proxy_excludes(default_service);
		if (!str_list)
			break;

		connman_dbus_dict_append_array(&dict, "Excludes",
					DBUS_TYPE_STRING, append_string_list,
					str_list);
		g_strfreev(str_list);

		break;
	case CONNMAN_SERVICE_PROXY_METHOD_AUTO:
		method = "auto";

		str = connman_service_get_proxy_url(default_service);
		if (!str) {
			str = connman_service_get_proxy_autoconfig(
							default_service);
			if (!str) {
				connman_dbus_dict_close(&iter, &dict);
				goto done;
			}
		}

		connman_dbus_dict_append_basic(&dict, "URL",
					DBUS_TYPE_STRING, &str);
		break;
	}

	connman_dbus_dict_append_basic(&dict, "Method",
				DBUS_TYPE_STRING, &method);

	interface = connman_service_get_interface(default_service);
	if (interface) {
		connman_dbus_dict_append_basic(&dict, "Interface",
						DBUS_TYPE_STRING, &interface);
		g_free(interface);
	}

	str_list = connman_service_get_nameservers(default_service);
	if (str_list)
		connman_dbus_dict_append_array(&dict, "Nameservers",
					DBUS_TYPE_STRING, append_string_list,
					str_list);
	g_strfreev(str_list);

	connman_dbus_dict_close(&iter, &dict);

	result = dbus_connection_send_with_reply(connection, msg,
							&call, DBUS_TIMEOUT);

	if (!result || !call)
		goto done;

	dbus_pending_call_set_notify(call, create_config_reply, NULL, NULL);

	dbus_pending_call_unref(call);

done:
	dbus_message_unref(msg);
}

static void destroy_config_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply = dbus_pending_call_steal_reply(call);

	DBG("");

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR)
		connman_error("Failed to destoy proxy configuration");

	dbus_message_unref(reply);
}

static void destroy_proxy_configuration(void)
{
	DBusMessage *msg;
	DBusPendingCall *call;
	dbus_bool_t result;

	if (!current_config)
		return;

	DBG("");

	msg = dbus_message_new_method_call(PACRUNNER_SERVICE, PACRUNNER_PATH,
			PACRUNNER_INTERFACE, "DestroyProxyConfiguration");
	if (!msg)
		return;

	dbus_message_set_auto_start(msg, FALSE);

	dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &current_config,
							DBUS_TYPE_INVALID);

	result = dbus_connection_send_with_reply(connection, msg,
							&call, DBUS_TIMEOUT);

	dbus_message_unref(msg);

	if (!result || !call)
		return;

	dbus_pending_call_set_notify(call, destroy_config_reply, NULL, NULL);

	dbus_pending_call_unref(call);

	g_free(current_config);
	current_config = NULL;
}

static void default_service_changed(struct connman_service *service)
{
	DBG("service %p", service);

	if (service == default_service)
		return;

	default_service = service;

	if (!daemon_running)
		return;

	destroy_proxy_configuration();

	create_proxy_configuration();
}

static void proxy_changed(struct connman_service *service)
{
	DBG("service %p", service);

	if (service != default_service)
		return;

	if (!daemon_running)
		return;

	destroy_proxy_configuration();

	create_proxy_configuration();
}

static const struct connman_notifier pacrunner_notifier = {
	.name			= "pacrunner",
	.default_changed	= default_service_changed,
	.proxy_changed		= proxy_changed,
};

static void pacrunner_connect(DBusConnection *conn, void *user_data)
{
	DBG("");

	daemon_running = TRUE;

	create_proxy_configuration();
}

static void pacrunner_disconnect(DBusConnection *conn, void *user_data)
{
	DBG("");

	daemon_running = FALSE;

	g_free(current_config);
	current_config = NULL;
}

static char * parse_url(const char *url)
{
	char *scheme, *host, *path, *host_ret;

	scheme = g_strdup(url);
	if (!scheme)
		return NULL;

	host = strstr(scheme, "://");
	if (host) {
		*host = '\0';
		host += 3;
	} else
		host = scheme;

	path = strchr(host, '/');
	if (path)
		*(path++) = '\0';

	host_ret = g_strdup(host);

	g_free(scheme);

	return host_ret;
}

static void request_lookup_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply = dbus_pending_call_steal_reply(call);
	struct proxy_data *data = user_data;
	const char *proxy;

	DBG("");

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		connman_error("Failed to find URL:%s", data->url);
		proxy = NULL;
		goto done;
	}

	if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_STRING,
					&proxy, DBUS_TYPE_INVALID))
		proxy = NULL;

done:
	connman_proxy_driver_lookup_notify(data->service, data->url, proxy);

	connman_service_unref(data->service);

	g_free(data->url);
	g_free(data);

	dbus_message_unref(reply);
}

static int request_lookup(struct connman_service *service, const char *url)
{
	DBusMessage *msg;
	DBusPendingCall *call;
	dbus_bool_t result;
	char *host;
	struct proxy_data *data;

	DBG("");

	msg = dbus_message_new_method_call(PACRUNNER_SERVICE,
						PACRUNNER_CLIENT_PATH,
						PACRUNNER_CLIENT_INTERFACE,
						"FindProxyForURL");
	if (!msg)
		return -1;

	host = parse_url(url);
	if (!host) {
		dbus_message_unref(msg);
		return -EINVAL;
	}

	data = g_try_new0(struct proxy_data, 1);
	if (!data) {
		dbus_message_unref(msg);
		g_free(host);
		return -ENOMEM;
	}

	data->url = g_strdup(url);
	data->service = connman_service_ref(service);

	dbus_message_append_args(msg, DBUS_TYPE_STRING, &url,
					DBUS_TYPE_STRING, &host,
					DBUS_TYPE_INVALID);

	result = dbus_connection_send_with_reply(connection, msg,
							&call, DBUS_TIMEOUT);

	dbus_message_unref(msg);

	if (!result || !call) {
		g_free(host);
		g_free(data->url);
		g_free(data);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(call, request_lookup_reply,
							data, NULL);

	dbus_pending_call_unref(call);
	g_free(host);

	return 0;
}

static void cancel_lookup(struct connman_service *service, const char *url)
{
	DBG("");
}

static struct connman_proxy_driver pacrunner_proxy = {
	.name		= "pacrunnerproxy",
	.priority	= CONNMAN_PROXY_PRIORITY_HIGH,
	.request_lookup	= request_lookup,
	.cancel_lookup	= cancel_lookup,
};

static guint pacrunner_watch;

static int pacrunner_init(void)
{
	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	pacrunner_watch = g_dbus_add_service_watch(connection,
					PACRUNNER_SERVICE, pacrunner_connect,
					pacrunner_disconnect, NULL, NULL);
	if (pacrunner_watch == 0) {
		dbus_connection_unref(connection);
		return -EIO;
	}

	connman_notifier_register(&pacrunner_notifier);

	connman_proxy_driver_register(&pacrunner_proxy);

	return 0;
}

static void pacrunner_exit(void)
{
	connman_proxy_driver_unregister(&pacrunner_proxy);

	connman_notifier_unregister(&pacrunner_notifier);

	g_dbus_remove_watch(connection, pacrunner_watch);

	destroy_proxy_configuration();

	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(pacrunner, "PAC runner proxy plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, pacrunner_init, pacrunner_exit)
