/*
 * oFono - Open Source Telephony
 * Copyright (C) 2008-2011  Intel Corporation
 *
 * SPDX-License-Identifier: GPL-2.0-only
 */

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

#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

#include <gdbus.h>
#include <string.h>

#define OFONO_API_SUBJECT_TO_CHANGE
#include <ofono/log.h>
#include <ofono/dbus.h>
#include <ofono/plugin.h>
#include <ofono/private-network.h>

#ifndef DBUS_TYPE_UNIX_FD
#define DBUS_TYPE_UNIX_FD -1
#endif

#define CONNMAN_SERVICE			"net.connman"
#define CONNMAN_PATH			"/net/connman"

#define CONNMAN_MANAGER_INTERFACE	CONNMAN_SERVICE ".Manager"
#define CONNMAN_MANAGER_PATH		"/"

static DBusConnection *connection;
static GHashTable *requests;
static unsigned int id;

struct connman_req {
	int uid;
	DBusPendingCall *pending;
	ofono_private_network_cb_t cb;
	void *data;
	gboolean redundant;
	char *path;
};

static void send_release(const char *path)
{
	DBusMessage *message;

	message = dbus_message_new_method_call(CONNMAN_SERVICE,
						CONNMAN_MANAGER_PATH,
						CONNMAN_MANAGER_INTERFACE,
						"ReleasePrivateNetwork");
	if (message == NULL)
		return;

	dbus_message_append_args(message, DBUS_TYPE_OBJECT_PATH, &path,
					DBUS_TYPE_INVALID);
	dbus_message_set_no_reply(message, TRUE);
	dbus_connection_send(connection, message, NULL);
	dbus_message_unref(message);
}

static void connman_release(int uid)
{
	struct connman_req *req;

	DBG("");

	req = g_hash_table_lookup(requests, &uid);
	if (req == NULL)
		return;

	if (req->pending) {
		/*
		* We want to cancel the request but we have to wait
		* the response of ConnMan. So we mark request as
		* redundant until we get the response, then we remove
		* it from hash table.
		*/
		req->redundant = TRUE;
		return;
	}

	send_release(req->path);
	g_hash_table_remove(requests, &req->uid);
}

static gboolean parse_reply(DBusMessage *reply, const char **path,
				struct ofono_private_network_settings *pns)
{
	DBusMessageIter array, dict, entry;

	if (!reply)
		return FALSE;

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR)
		return FALSE;

	if (dbus_message_iter_init(reply, &array) == FALSE)
		return FALSE;

	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_OBJECT_PATH)
		return FALSE;

	dbus_message_iter_get_basic(&array, path);

	dbus_message_iter_next(&array);
	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY)
		return FALSE;

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter iter;
		const char *key;
		int type;

		dbus_message_iter_recurse(&dict, &entry);

		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &iter);

		type = dbus_message_iter_get_arg_type(&iter);
		if (type != DBUS_TYPE_STRING)
			break;

		if (g_str_equal(key, "ServerIPv4") &&
				type == DBUS_TYPE_STRING)
			dbus_message_iter_get_basic(&iter, &pns->server_ip);
		else if (g_str_equal(key, "PeerIPv4") &&
				type == DBUS_TYPE_STRING)
			dbus_message_iter_get_basic(&iter, &pns->peer_ip);
		else if (g_str_equal(key, "PrimaryDNS") &&
				type == DBUS_TYPE_STRING)
			dbus_message_iter_get_basic(&iter, &pns->primary_dns);
		else if (g_str_equal(key, "SecondaryDNS") &&
				type == DBUS_TYPE_STRING)
			dbus_message_iter_get_basic(&iter, &pns->secondary_dns);

		dbus_message_iter_next(&dict);
	}

	dbus_message_iter_next(&array);
	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_UNIX_FD)
		return FALSE;

	dbus_message_iter_get_basic(&array, &pns->fd);

	return TRUE;
}

static void request_reply(DBusPendingCall *call, void *user_data)
{
	struct connman_req *req = user_data;
	DBusMessage *reply;
	const char *path = NULL;
	struct ofono_private_network_settings pns;

	DBG("");

	req->pending = NULL;

	memset(&pns, 0, sizeof(pns));
	pns.fd = -1;

	reply = dbus_pending_call_steal_reply(call);
	if (reply == NULL)
		goto badreply;

	if (parse_reply(reply, &path, &pns) == FALSE)
		goto error;

	DBG("fd: %d, path: %s", pns.fd, path);

	if (req->redundant == TRUE)
		goto redundant;

	if (pns.server_ip == NULL || pns.peer_ip == NULL ||
			pns.primary_dns == NULL || pns.secondary_dns == NULL ||
			pns.fd < 0) {
		ofono_error("Error while reading dictionary...\n");
		goto error;
	}

	req->path = g_strdup(path);
	req->cb(&pns, req->data);

	dbus_message_unref(reply);
	dbus_pending_call_unref(call);
	return;

error:
redundant:
	if (pns.fd != -1)
		close(pns.fd);

	if (path != NULL)
		send_release(path);

	dbus_message_unref(reply);

badreply:
	if (req->redundant == FALSE)
		req->cb(NULL, req->data);

	g_hash_table_remove(requests, &req->uid);
	dbus_pending_call_unref(call);
}

static int connman_request(ofono_private_network_cb_t cb, void *data)
{
	DBusMessage *message;
	DBusPendingCall *call;
	struct connman_req *req;

	DBG("");

	if (DBUS_TYPE_UNIX_FD < 0)
		return -EBADF;

	req = g_try_new(struct connman_req, 1);
	if (req == NULL)
		return -ENOMEM;

	message = dbus_message_new_method_call(CONNMAN_SERVICE,
						CONNMAN_MANAGER_PATH,
						CONNMAN_MANAGER_INTERFACE,
						"RequestPrivateNetwork");

	if (message == NULL) {
		g_free(req);
		return -ENOMEM;
	}

	if (dbus_connection_send_with_reply(connection, message,
						&call, 5000) == FALSE) {
		g_free(req);
		dbus_message_unref(message);
		return -EIO;
	}

	id++;
	req->pending = call;
	req->cb = cb;
	req->data = data;
	req->uid = id;
	req->redundant = FALSE;
	req->path = NULL;

	dbus_pending_call_set_notify(call, request_reply, req, NULL);
	g_hash_table_insert(requests, &req->uid, req);
	dbus_message_unref(message);

	return req->uid;
}

static struct ofono_private_network_driver pn_driver = {
	.name		= "ConnMan Private Network",
	.request	= connman_request,
	.release	= connman_release,
};

static void request_free(gpointer user_data)
{
	struct connman_req *req = user_data;

	g_free(req->path);
	g_free(req);
}

static int connman_init(void)
{
	DBG("");

	connection = ofono_dbus_get_connection();
	requests = g_hash_table_new_full(g_int_hash, g_int_equal, NULL,
						request_free);

	return ofono_private_network_driver_register(&pn_driver);
}

static void connman_exit(void)
{
	g_hash_table_destroy(requests);
	ofono_private_network_driver_unregister(&pn_driver);
}

OFONO_PLUGIN_DEFINE(connman, "ConnMan plugin", VERSION,
		OFONO_PLUGIN_PRIORITY_DEFAULT, connman_init, connman_exit)
