/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 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 <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

#include "connman.h"

#include <gdhcp/gdhcp.h>

#define DEFAULT_ROUTER_LIFETIME 180  /* secs */
#define DEFAULT_RA_INTERVAL 120  /* secs */

static int bridge_index = -1;
static guint timer_uplink;
static guint timer_ra;
static char *default_interface;
static GSList *prefixes;
static void *rs_context;

static int setup_prefix_delegation(struct connman_service *service);
static void dhcpv6_callback(struct connman_network *network,
			enum __connman_dhcpv6_status status, gpointer data);

static int enable_ipv6_forward(bool enable)
{
	FILE *f;

	f = fopen("/proc/sys/net/ipv6/ip_forward", "r+");
	if (!f)
		return -errno;

	if (enable)
		fprintf(f, "1");
	else
		fprintf(f, "0");

	fclose(f);

	return 0;
}

static gboolean send_ra(gpointer data)
{
	__connman_inet_ipv6_send_ra(bridge_index, NULL, prefixes,
					DEFAULT_ROUTER_LIFETIME);

	return TRUE;
}

static void start_ra(int ifindex, GSList *prefix)
{
	if (prefixes)
		g_slist_free_full(prefixes, g_free);

	prefixes = g_dhcpv6_copy_prefixes(prefix);

	enable_ipv6_forward(true);

	if (timer_ra > 0)
		g_source_remove(timer_ra);

	send_ra(NULL);

	timer_ra = g_timeout_add_seconds(DEFAULT_RA_INTERVAL, send_ra, NULL);
}

static void stop_ra(int ifindex)
{
	__connman_inet_ipv6_send_ra(ifindex, NULL, prefixes, 0);

	if (timer_ra > 0) {
		g_source_remove(timer_ra);
		timer_ra = 0;
	}

	enable_ipv6_forward(false);

	if (prefixes) {
		g_slist_free_full(prefixes, g_free);
		prefixes = NULL;
	}
}

static void rs_received(struct nd_router_solicit *reply,
			unsigned int length, void *user_data)
{
	GDHCPIAPrefix *prefix;
	GSList *list;

	if (!prefixes)
		return;

	DBG("");

	for (list = prefixes; list; list = list->next) {
		prefix = list->data;

		prefix->valid -= time(NULL) - prefix->expire;
		prefix->preferred -= time(NULL) - prefix->expire;
	}

	__connman_inet_ipv6_send_ra(bridge_index, NULL, prefixes,
					DEFAULT_ROUTER_LIFETIME);
}

static gboolean do_setup(gpointer data)
{
	int ret;

	timer_uplink = 0;

	if (!default_interface)
		DBG("No uplink connection, retrying prefix delegation");

	ret = setup_prefix_delegation(connman_service_get_default());
	if (ret < 0 && ret != -EINPROGRESS)
		return TRUE; /* delegation error, try again */

	return FALSE;
}

static void dhcpv6_renew_callback(struct connman_network *network,
				enum __connman_dhcpv6_status status,
				gpointer data)
{
	DBG("network %p status %d data %p", network, status, data);

	if (status == CONNMAN_DHCPV6_STATUS_SUCCEED)
		dhcpv6_callback(network, status, data);
	else
		setup_prefix_delegation(connman_service_get_default());
}

static void cleanup(void)
{
	if (timer_uplink != 0) {
		g_source_remove(timer_uplink);
		timer_uplink = 0;
	}

	if (prefixes) {
		g_slist_free_full(prefixes, g_free);
		prefixes = NULL;
	}
}

static void dhcpv6_callback(struct connman_network *network,
			enum __connman_dhcpv6_status status, gpointer data)
{
	GSList *prefix_list = data;

	DBG("network %p status %d data %p", network, status, data);

	if (status == CONNMAN_DHCPV6_STATUS_FAIL) {
		DBG("Prefix delegation request failed");
		cleanup();
		return;
	}

	if (!prefix_list) {
		DBG("No prefixes, retrying");
		if (timer_uplink == 0)
			timer_uplink = g_timeout_add_seconds(10, do_setup,
									NULL);
		return;
	}

	/*
	 * After we have got a list of prefixes, we can start to send router
	 * advertisements (RA) to tethering interface.
	 */
	start_ra(bridge_index, prefix_list);

	if (__connman_dhcpv6_start_pd_renew(network,
					dhcpv6_renew_callback) == -ETIMEDOUT)
		dhcpv6_renew_callback(network, CONNMAN_DHCPV6_STATUS_FAIL,
									NULL);
}

static int setup_prefix_delegation(struct connman_service *service)
{
	struct connman_ipconfig *ipconfig;
	char *interface;
	int err = 0, ifindex;

	if (!service) {
		/*
		 * We do not have uplink connection. We just wait until
		 * default interface is updated.
		 */
		return -EINPROGRESS;
	}

	interface = connman_service_get_interface(service);

	DBG("interface %s bridge_index %d", interface, bridge_index);

	if (default_interface) {
		stop_ra(bridge_index);

		ifindex = connman_inet_ifindex(default_interface);
		__connman_dhcpv6_stop_pd(ifindex);
	}

	g_free(default_interface);

	ipconfig = __connman_service_get_ip6config(service);
	if (!__connman_ipconfig_ipv6_is_enabled(ipconfig)) {
		g_free(interface);
		default_interface = NULL;
		return -EPFNOSUPPORT;
	}

	default_interface = interface;

	if (default_interface) {
		ifindex = connman_inet_ifindex(default_interface);

		/*
		 * Try to get a IPv6 prefix so we can start to advertise it.
		 */
		err = __connman_dhcpv6_start_pd(ifindex, prefixes,
						dhcpv6_callback);
		if (err < 0)
			DBG("prefix delegation %d/%s", err, strerror(-err));
	}

	return err;
}

static void update_default_interface(struct connman_service *service)
{
	setup_prefix_delegation(service);
}

static void update_ipconfig(struct connman_service *service,
				struct connman_ipconfig *ipconfig)
{
	if (!service || service != connman_service_get_default())
		return;

	if (ipconfig != __connman_service_get_ip6config(service))
		return;

	if (!__connman_ipconfig_ipv6_is_enabled(ipconfig)) {
		if (default_interface) {
			int ifindex;

			ifindex = connman_inet_ifindex(default_interface);
			__connman_dhcpv6_stop_pd(ifindex);

			g_free(default_interface);
			default_interface = NULL;
		}

		DBG("No IPv6 support for interface index %d",
			__connman_ipconfig_get_index(ipconfig));
		return;
	}

	/*
	 * Did we had PD activated already? If not, then start it.
	 */
	if (!default_interface) {
		DBG("IPv6 ipconfig %p changed for interface index %d", ipconfig,
			__connman_ipconfig_get_index(ipconfig));

		setup_prefix_delegation(service);
	}
}

static const struct connman_notifier pd_notifier = {
	.name			= "IPv6 prefix delegation",
	.default_changed	= update_default_interface,
	.ipconfig_changed	= update_ipconfig,
};

int __connman_ipv6pd_setup(const char *bridge)
{
	struct connman_service *service;
	int err;

	if (!connman_inet_is_ipv6_supported())
		return -EPFNOSUPPORT;

	if (bridge_index >= 0) {
		DBG("Prefix delegation already running");
		return -EALREADY;
	}

	err = connman_notifier_register(&pd_notifier);
	if (err < 0)
		return err;

	bridge_index = connman_inet_ifindex(bridge);

	err = __connman_inet_ipv6_start_recv_rs(bridge_index, rs_received,
						NULL, &rs_context);
	if (err < 0)
		DBG("Cannot receive router solicitation %d/%s",
			err, strerror(-err));

	service = connman_service_get_default();
	if (service) {
		/*
		 * We have an uplink connection already, try to use it.
		 */
		return setup_prefix_delegation(service);
	}

	/*
	 * The prefix delegation is started after have got the uplink
	 * connection up i.e., when the default service is setup in which
	 * case the update_default_interface() will be called.
	 */
	return -EINPROGRESS;
}

void __connman_ipv6pd_cleanup(void)
{
	int ifindex;

	if (!connman_inet_is_ipv6_supported())
		return;

	connman_notifier_unregister(&pd_notifier);

	__connman_inet_ipv6_stop_recv_rs(rs_context);
	rs_context = NULL;

	cleanup();

	stop_ra(bridge_index);

	if (default_interface) {
		ifindex = connman_inet_ifindex(default_interface);
		__connman_dhcpv6_stop_pd(ifindex);
		g_free(default_interface);
		default_interface = NULL;
	}

	bridge_index = -1;
}
