/*
 *
 *  ConnMan VPN daemon
 *
 *  Copyright (C) 2012  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 <stdio.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <linux/if_link.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h>

#ifndef IFF_LOWER_UP
#define IFF_LOWER_UP	0x10000
#endif

#include <gdbus.h>

#include "../src/connman.h"

#include "vpn.h"

struct vpn_ipconfig {
	int refcount;
	int index;
	int family;
	bool enabled;
	struct connman_ipaddress *address;
	struct connman_ipaddress *system;
};

struct vpn_ipdevice {
	int index;
	char *ifname;
	unsigned short type;
	unsigned int flags;
	char *address;
	uint16_t mtu;

	GSList *address_list;
	char *ipv4_gateway;
	char *ipv6_gateway;

	char *pac;
};

static GHashTable *ipdevice_hash = NULL;

struct connman_ipaddress *
__vpn_ipconfig_get_address(struct vpn_ipconfig *ipconfig)
{
	if (!ipconfig)
		return NULL;

	return ipconfig->address;
}

const char *__vpn_ipconfig_get_peer(struct vpn_ipconfig *ipconfig)
{
	if (!ipconfig->address)
		return NULL;

	return ipconfig->address->peer;
}

unsigned short __vpn_ipconfig_get_type_from_index(int index)
{
	struct vpn_ipdevice *ipdevice;

	ipdevice = g_hash_table_lookup(ipdevice_hash, GINT_TO_POINTER(index));
	if (!ipdevice)
		return ARPHRD_VOID;

	return ipdevice->type;
}

unsigned int __vpn_ipconfig_get_flags_from_index(int index)
{
	struct vpn_ipdevice *ipdevice;

	ipdevice = g_hash_table_lookup(ipdevice_hash, GINT_TO_POINTER(index));
	if (!ipdevice)
		return 0;

	return ipdevice->flags;
}

void vpn_ipconfig_foreach(void (*function) (int index,
					void *user_data), void *user_data)
{
	GList *list, *keys;

	keys = g_hash_table_get_keys(ipdevice_hash);
	if (!keys)
		return;

	for (list = g_list_first(keys); list; list = g_list_next(list)) {
		int index = GPOINTER_TO_INT(list->data);

		function(index, user_data);
	}

	g_list_free(keys);
}

void __vpn_ipconfig_set_local(struct vpn_ipconfig *ipconfig,
							const char *address)
{
	if (!ipconfig->address)
		return;

	g_free(ipconfig->address->local);
	ipconfig->address->local = g_strdup(address);
}

const char *__vpn_ipconfig_get_local(struct vpn_ipconfig *ipconfig)
{
	if (!ipconfig->address)
		return NULL;

	return ipconfig->address->local;
}

void __vpn_ipconfig_set_peer(struct vpn_ipconfig *ipconfig,
							const char *address)
{
	if (!ipconfig->address)
		return;

	g_free(ipconfig->address->peer);
	ipconfig->address->peer = g_strdup(address);
}

void __vpn_ipconfig_set_broadcast(struct vpn_ipconfig *ipconfig,
					const char *broadcast)
{
	if (!ipconfig->address)
		return;

	g_free(ipconfig->address->broadcast);
	ipconfig->address->broadcast = g_strdup(broadcast);
}

void __vpn_ipconfig_set_gateway(struct vpn_ipconfig *ipconfig,
							const char *gateway)
{
	DBG("");

	if (!ipconfig->address)
		return;
	g_free(ipconfig->address->gateway);
	ipconfig->address->gateway = g_strdup(gateway);
}

const char *
__vpn_ipconfig_get_gateway(struct vpn_ipconfig *ipconfig)
{
	if (!ipconfig->address)
		return NULL;

	return ipconfig->address->gateway;
}

void __vpn_ipconfig_set_prefixlen(struct vpn_ipconfig *ipconfig,
					unsigned char prefixlen)
{
	if (!ipconfig->address)
		return;

	ipconfig->address->prefixlen = prefixlen;
}

unsigned char
__vpn_ipconfig_get_prefixlen(struct vpn_ipconfig *ipconfig)
{
	if (!ipconfig->address)
		return 0;

	return ipconfig->address->prefixlen;
}

int __vpn_ipconfig_address_add(struct vpn_ipconfig *ipconfig, int family)
{
	DBG("ipconfig %p family %d", ipconfig, family);

	if (!ipconfig)
		return -EINVAL;

	if (family == AF_INET)
		return connman_inet_set_address(ipconfig->index,
							ipconfig->address);
	else if (family == AF_INET6)
		return connman_inet_set_ipv6_address(ipconfig->index,
							ipconfig->address);

	return 0;
}

int __vpn_ipconfig_gateway_add(struct vpn_ipconfig *ipconfig, int family)
{
	DBG("ipconfig %p family %d", ipconfig, family);

	if (!ipconfig || !ipconfig->address)
		return -EINVAL;

	DBG("family %d gw %s peer %s", family,
		ipconfig->address->gateway, ipconfig->address->peer);

	if (family == AF_INET)
		connman_inet_add_host_route(ipconfig->index,
					ipconfig->address->gateway,
					ipconfig->address->peer);
	else if (family == AF_INET6)
		connman_inet_add_ipv6_host_route(ipconfig->index,
						ipconfig->address->gateway,
						ipconfig->address->peer);
	else
		return -EINVAL;

	return 0;
}

void __vpn_ipconfig_unref_debug(struct vpn_ipconfig *ipconfig,
				const char *file, int line, const char *caller)
{
	if (!ipconfig)
		return;

	DBG("%p ref %d by %s:%d:%s()", ipconfig, ipconfig->refcount - 1,
		file, line, caller);

	if (__sync_fetch_and_sub(&ipconfig->refcount, 1) != 1)
		return;

	connman_ipaddress_free(ipconfig->system);
	connman_ipaddress_free(ipconfig->address);
	g_free(ipconfig);
}

static struct vpn_ipconfig *create_ipv6config(int index)
{
	struct vpn_ipconfig *ipv6config;

	DBG("index %d", index);

	ipv6config = g_try_new0(struct vpn_ipconfig, 1);
	if (!ipv6config)
		return NULL;

	ipv6config->refcount = 1;

	ipv6config->index = index;
	ipv6config->enabled = false;
	ipv6config->family = AF_INET6;

	ipv6config->address = connman_ipaddress_alloc(AF_INET6);
	if (!ipv6config->address) {
		g_free(ipv6config);
		return NULL;
	}

	connman_ipaddress_set_p2p(ipv6config->address, true);

	ipv6config->system = connman_ipaddress_alloc(AF_INET6);
	connman_ipaddress_set_p2p(ipv6config->system, true);

	DBG("ipconfig %p", ipv6config);

	return ipv6config;
}

struct vpn_ipconfig *__vpn_ipconfig_create(int index, int family)
{
	struct vpn_ipconfig *ipconfig;

	if (family == AF_INET6)
		return create_ipv6config(index);

	DBG("index %d", index);

	ipconfig = g_try_new0(struct vpn_ipconfig, 1);
	if (!ipconfig)
		return NULL;

	ipconfig->refcount = 1;

	ipconfig->index = index;
	ipconfig->enabled = false;
	ipconfig->family = family;

	ipconfig->address = connman_ipaddress_alloc(AF_INET);
	if (!ipconfig->address) {
		g_free(ipconfig);
		return NULL;
	}

	connman_ipaddress_set_p2p(ipconfig->address, true);

	ipconfig->system = connman_ipaddress_alloc(AF_INET);
	connman_ipaddress_set_p2p(ipconfig->system, true);

	DBG("ipconfig %p", ipconfig);

	return ipconfig;
}

void __vpn_ipconfig_set_index(struct vpn_ipconfig *ipconfig, int index)
{
	ipconfig->index = index;
}

static const char *type2str(unsigned short type)
{
	switch (type) {
	case ARPHRD_ETHER:
		return "ETHER";
	case ARPHRD_LOOPBACK:
		return "LOOPBACK";
	case ARPHRD_PPP:
		return "PPP";
	case ARPHRD_NONE:
		return "NONE";
	case ARPHRD_VOID:
		return "VOID";
	}

	return "";
}

void __vpn_ipconfig_newlink(int index, unsigned short type,
				unsigned int flags,
				const char *address,
				unsigned short mtu,
				struct rtnl_link_stats *stats)
{
	struct vpn_ipdevice *ipdevice;
	GString *str;

	if (type == ARPHRD_LOOPBACK)
		return;

	ipdevice = g_hash_table_lookup(ipdevice_hash, GINT_TO_POINTER(index));
	if (ipdevice)
		goto update;

	ipdevice = g_try_new0(struct vpn_ipdevice, 1);
	if (!ipdevice)
		return;

	ipdevice->index = index;
	ipdevice->ifname = connman_inet_ifname(index);
	ipdevice->type = type;

	ipdevice->address = g_strdup(address);

	g_hash_table_insert(ipdevice_hash, GINT_TO_POINTER(index), ipdevice);

	connman_info("%s {create} index %d type %d <%s>", ipdevice->ifname,
						index, type, type2str(type));

update:
	ipdevice->mtu = mtu;

	if (flags == ipdevice->flags)
		return;

	ipdevice->flags = flags;

	str = g_string_new(NULL);
	if (!str)
		return;

	if (flags & IFF_UP)
		g_string_append(str, "UP");
	else
		g_string_append(str, "DOWN");

	if (flags & IFF_RUNNING)
		g_string_append(str, ",RUNNING");

	if (flags & IFF_LOWER_UP)
		g_string_append(str, ",LOWER_UP");

	connman_info("%s {update} flags %u <%s>", ipdevice->ifname,
							flags, str->str);

	g_string_free(str, TRUE);
}

void __vpn_ipconfig_dellink(int index, struct rtnl_link_stats *stats)
{
	struct vpn_ipdevice *ipdevice;

	DBG("index %d", index);

	ipdevice = g_hash_table_lookup(ipdevice_hash, GINT_TO_POINTER(index));
	if (!ipdevice)
		return;

	g_hash_table_remove(ipdevice_hash, GINT_TO_POINTER(index));
}

static void free_ipdevice(gpointer data)
{
	struct vpn_ipdevice *ipdevice = data;

	connman_info("%s {remove} index %d", ipdevice->ifname,
							ipdevice->index);

	g_free(ipdevice->ipv4_gateway);
	g_free(ipdevice->ipv6_gateway);
	g_free(ipdevice->pac);

	g_free(ipdevice->address);

	g_free(ipdevice->ifname);
	g_free(ipdevice);
}

int __vpn_ipconfig_init(void)
{
	DBG("");

	ipdevice_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
							NULL, free_ipdevice);

	return 0;
}

void __vpn_ipconfig_cleanup(void)
{
	DBG("");

	g_hash_table_destroy(ipdevice_hash);
	ipdevice_hash = NULL;
}
