/*
   *  Copyright (c) 2007, 2008, Andrea Bittau <a.bittau@cs.ucl.ac.uk>
   *
   *  OS dependent API for cygwin. TAP routines
   *
   *  This program is free software; you can redistribute it and/or modify
   *  it under the terms of the GNU General Public License as published by
   *  the Free Software Foundation; either version 2 of the License, or
   *  (at your option) any later version.
   *
   *  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 <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>

#include "osdep.h"

#include <windows.h>
#include <winioctl.h>
#include <ipexport.h>
#include <iptypes.h>
#include <initguid.h>
#include <devguid.h>
#include <setupapi.h>

#include "network.h"
#include "tap-win32/common.h"

extern DWORD WINAPI GetAdaptersInfo(PIP_ADAPTER_INFO pAdapterInfo,
									PULONG pOutBufLen);
extern DWORD WINAPI AddIPAddress(IPAddr Address,
								 IPMask IpMask,
								 DWORD IfIndex,
								 PULONG NTEContext,
								 PULONG NTEInstance);
extern DWORD WINAPI DeleteIPAddress(ULONG NTEContext);

extern int cygwin_read_reader(int fd, int plen, void * dst, int len);
static void * ti_reader(void * arg);

struct tip_cygwin
{
	char tc_name[256];
	HANDLE tc_h;
	pthread_t tc_reader;
	volatile int tc_running;
	int tc_pipe[2]; /* reader -> parent */
	pthread_mutex_t tc_mtx;
	HKEY tc_key;
	char tc_guid[256];
};

/**
 * Stop the reader thread (if it is running)
 * @return 0 if stopped or -1 if it failed to stop it
 */
static int stop_reader(struct tip_cygwin * priv)
{
	if (priv->tc_running == 1)
	{
		int tries = 3;

		priv->tc_running = 0;
		while ((priv->tc_running != -1) && tries--) sleep(1);

		if (tries <= 0) return -1;
	}

	return 0;
}

/**
 * Start reader thread
 * @return -1 if failed to start thread or 0 if it is successful
 */
static int start_reader(struct tip_cygwin * priv)
{
	priv->tc_running = 2;
	if (pthread_create(&priv->tc_reader, NULL, ti_reader, priv)) return -1;

	priv->tc_running = 1;

	return 0;
}

/**
 * Change status (enable/disable) of the device
 */
static int ti_media_status(struct tip_cygwin * priv, int on)
{
	ULONG s = on;
	DWORD len;

	if (!DeviceIoControl(priv->tc_h,
						 TAP_IOCTL_SET_MEDIA_STATUS,
						 &s,
						 sizeof(s),
						 &s,
						 sizeof(s),
						 &len,
						 NULL))
		return -1;

	return 0;
}

/**
 * Try opening device
 */
static int ti_try_open(struct tip_cygwin * priv, char * guid)
{
	int any = priv->tc_guid[0] == 0;
	char device[256];
	HANDLE h;

	if (!any && strcmp(priv->tc_guid, guid) != 0) return 0;

	/* open the device */
	const ssize_t device_len = snprintf(
		device, sizeof(device), "%s%s%s", USERMODEDEVICEDIR, guid, TAPSUFFIX);
	if (device_len == -1 || (size_t) device_len >= sizeof(device)) return -1;
	h = CreateFile(device,
				   GENERIC_READ | GENERIC_WRITE,
				   0,
				   0,
				   OPEN_EXISTING,
				   FILE_ATTRIBUTE_SYSTEM | FILE_FLAG_OVERLAPPED,
				   0);
	if (h == INVALID_HANDLE_VALUE)
	{
		if (any)
			return 0;
		else
			return -1;
	}
	priv->tc_h = h;

	/* XXX check tap version */

	/* bring iface up */
	if (ti_media_status(priv, 1) == -1) goto out_err;

	/* grab printable name */
	ssize_t err = snprintf(priv->tc_name, sizeof(priv->tc_name) - 1, "%s", guid);
	if (err == -1 || (size_t) err >= sizeof(priv->tc_name)) goto out_err;

	if (any)
	{
		err = snprintf(priv->tc_guid, sizeof(priv->tc_guid), "%s", guid);
		if (err == -1 || (size_t) err >= sizeof(priv->tc_guid)) goto out_err;
	}

	return 1;

out_err:
	CloseHandle(priv->tc_h);
	priv->tc_h = INVALID_HANDLE_VALUE;
	return -1;
}

/**
 * Read registry value
 * @param key Registry key
 * @return 0 if successful, -1 if it failed
 */
static int
ti_read_reg(struct tip_cygwin * priv, char * key, char * res, int len)
{
	DWORD dt, l = len;

	if (RegQueryValueEx(priv->tc_key, key, NULL, &dt, (unsigned char *) res, &l)
		!= ERROR_SUCCESS)
		return -1;

	if (dt != REG_SZ) return -1;

	if ((int) l > len) return -1;

	return 0;
}

static int ti_get_devs_component(struct tip_cygwin * priv, char * name)
{
	char key[256];
	int rc = 0;

	const ssize_t key_len
		= snprintf(key, sizeof(key) - 1, "%s\\%s", ADAPTER_KEY, name);
	if (key_len == -1 || (size_t) key_len >= sizeof(key)) return -1;
	if (RegOpenKeyEx(
			HKEY_LOCAL_MACHINE, key, 0, KEY_READ | KEY_WRITE, &priv->tc_key)
		!= ERROR_SUCCESS)
		return -1;

	if (ti_read_reg(priv, "ComponentId", key, sizeof(key)) == -1) goto out;

	/* make sure component id matches */
	if (strcmp(key, TAP_COMPONENT_ID) != 0) goto out;

	/* get guid */
	if (ti_read_reg(priv, "NetCfgInstanceId", key, sizeof(key)) == -1) goto out;

	rc = ti_try_open(priv, key);

out:
	if (rc != 1)
	{
		RegCloseKey(priv->tc_key);
		priv->tc_key = 0;
	}

	return rc;
}

static int ti_do_open_cygwin(struct tip_cygwin * priv)
{
	int rc = -1;
	HKEY ak47;
	int i;
	char name[256];
	DWORD len;

	/* open network driver key */
	if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, ADAPTER_KEY, 0, KEY_READ, &ak47)
		!= ERROR_SUCCESS)
		return -1;

	/* find tap */
	for (i = 0;; i++)
	{
		len = sizeof(name);
		if (RegEnumKeyEx(ak47, i, name, &len, NULL, NULL, NULL, NULL)
			!= ERROR_SUCCESS)
			break;

		rc = ti_get_devs_component(priv, name);
		if (rc) break;
		rc = -1;
	}

	RegCloseKey(ak47);

	if (rc == 1) rc = 0;

	return rc;
}

static void ti_do_free(struct tif * ti)
{
	struct tip_cygwin * priv = ti_priv(ti);

	/* stop reader */
	stop_reader(priv);

	if (priv->tc_pipe[0])
	{
		close(priv->tc_pipe[0]);
		close(priv->tc_pipe[1]);
	}

	/* close card */
	if (priv->tc_h)
	{
		ti_media_status(priv, 0);
		CloseHandle(priv->tc_h);
	}

	if (priv->tc_key) RegCloseKey(priv->tc_key);

	free(priv);
	free(ti);
}

static void ti_close_cygwin(struct tif * ti) { ti_do_free(ti); }

static char * ti_name_cygwin(struct tif * ti)
{
	struct tip_cygwin * priv = ti_priv(ti);

	return priv->tc_name;
}

/* XXX */
static int
ti_is_us(struct tip_cygwin * priv, HDEVINFO * hdi, SP_DEVINFO_DATA * did)
{
	char buf[256];
	DWORD len = sizeof(buf), dt;

	if (priv)
	{
	} /* XXX unused */

	if (!SetupDiGetDeviceRegistryProperty(
			*hdi, did, SPDRP_DEVICEDESC, &dt, (unsigned char *) buf, len, &len))
		return 0;

	if (dt != REG_SZ) return 0;

	return strstr(buf, "TAP-Win32") != NULL;
}

static int ti_reset_state(HDEVINFO * hdi, SP_DEVINFO_DATA * did, DWORD state)
{
	SP_PROPCHANGE_PARAMS parm;

	parm.ClassInstallHeader.cbSize = sizeof(parm.ClassInstallHeader);
	parm.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
	parm.Scope = DICS_FLAG_GLOBAL;
	parm.StateChange = state;

	if (!SetupDiSetClassInstallParams(
			*hdi, did, (SP_CLASSINSTALL_HEADER *) &parm, sizeof(parm)))
		return -1;

	if (!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, *hdi, did)) return -1;

	return 0;
}

/**
 * Reset the device
 * @return 0 if successful, -1 if it failed
 */
static int ti_do_reset(HDEVINFO * hdi, SP_DEVINFO_DATA * did)
{
	int rc;

	rc = ti_reset_state(hdi, did, DICS_DISABLE);
	if (rc) return rc;

	return ti_reset_state(hdi, did, DICS_ENABLE);
}

static int ti_restart(struct tip_cygwin * priv)
{
	/* kill handle to if */
	if (priv->tc_h) CloseHandle(priv->tc_h);

	/* stop reader */
	if (stop_reader(priv)) return -1;

	/* reopen dev */
	if (ti_do_open_cygwin(priv)) return -1;

	return start_reader(priv);
}

static int ti_reset(struct tip_cygwin * priv)
{
	HDEVINFO hdi;
	SP_DEVINFO_DATA did;
	int i;
	int rc = -1;

	hdi = SetupDiGetClassDevs(&GUID_DEVCLASS_NET, NULL, NULL, DIGCF_PRESENT);
	if (hdi == INVALID_HANDLE_VALUE) return -1;

	/* find device */
	for (i = 0;; i++)
	{
		did.cbSize = sizeof(did);
		if (!SetupDiEnumDeviceInfo(hdi, i, &did)) break;

		if (!ti_is_us(priv, &hdi, &did)) continue;

		rc = ti_do_reset(&hdi, &did);
		if (rc) break;

		rc = ti_restart(priv);
		break;
	}

	SetupDiDestroyDeviceInfoList(hdi);

	return rc;
}

static int ti_set_mtu_cygwin(struct tif * ti, int mtu)
{
	struct tip_cygwin * priv = ti_priv(ti);
	char m[16];
	char mold[sizeof(m)];
	char * key = "MTU";

	/* check if reg remains unchanged to avoid reset */
	const ssize_t m_len = snprintf(m, sizeof(m) - 1, "%d", mtu);
	if (m_len == -1 || (size_t) m_len >= sizeof(m)) return -1;
	if (ti_read_reg(priv, key, mold, sizeof(mold)) != -1)
	{
		if (strcmp(m, mold) == 0) return 0;
	}

	/* change */
	if (RegSetValueEx(
			priv->tc_key, key, 0, REG_SZ, (unsigned char *) m, strlen(m) + 1)
		!= ERROR_SUCCESS)
		return -1;

	if (ti_reset(priv) == -1) return -1;

	return 0;
}

/**
 * Set device MAC address
 * @param mac New MAC address
 * @return -1 if it failed, 0 on success
 */
static int ti_set_mac_cygwin(struct tif * ti, unsigned char * mac)
{
	struct tip_cygwin * priv = ti_priv(ti);
	char str[2 * 6 + 1];
	char strold[sizeof(str)];
	int i;
	char * key = "MAC";

	/* convert */
	str[0] = 0;
	for (i = 0; i < 6; i++)
	{
		char tmp[3];

		if (sprintf(tmp, "%.2X", *mac++) != 2) return -1;
		strcat(str, tmp);
	}

	/* check if changed */
	if (ti_read_reg(priv, key, strold, sizeof(strold)) != -1)
	{
		if (strcmp(str, strold) == 0) return 0;
	}

	/* own */
	if (RegSetValueEx(priv->tc_key,
					  key,
					  0,
					  REG_SZ,
					  (unsigned char *) str,
					  strlen(str) + 1)
		!= ERROR_SUCCESS)
		return -1;

	if (ti_reset(priv) == -1) return -1;

	return 0;
}

/**
 * Set device IP address
 * @param ip New IP address
 * @return -1 if it failed, 0 on success
 */
static int ti_set_ip_cygwin(struct tif * ti, struct in_addr * ip)
{
	struct tip_cygwin * priv = ti_priv(ti);
	ULONG ctx, inst;
	IP_ADAPTER_INFO ai[16];
	DWORD len = sizeof(ai);
	PIP_ADAPTER_INFO p;
	PIP_ADDR_STRING ips;

	if (GetAdaptersInfo(ai, &len) != ERROR_SUCCESS) return -1;

	p = ai;
	while (p)
	{
		if (strcmp(priv->tc_guid, p->AdapterName) != 0)
		{
			p = p->Next;
			continue;
		}

		/* delete ips */
		ips = &p->IpAddressList;
		while (ips)
		{
			DeleteIPAddress(ips->Context);
			ips = ips->Next;
		}

		/* add ip */
		if (AddIPAddress(ip->s_addr, htonl(0xffffff00), p->Index, &ctx, &inst)
			!= NO_ERROR)
			return -1;

		break;
	}

	return 0;
}

static int ti_fd_cygwin(struct tif * ti)
{
	struct tip_cygwin * priv = ti_priv(ti);

	return priv->tc_pipe[0];
}

static int ti_read_cygwin(struct tif * ti, void * buf, int len)
{
	struct tip_cygwin * priv = ti_priv(ti);
	int plen;

	if (priv->tc_running != 1) return -1;

	/* read len */
	if (net_read_exact(priv->tc_pipe[0], &plen, sizeof(plen)) == -1) return -1;

	return cygwin_read_reader(priv->tc_pipe[0], plen, buf, len);
}

static int ti_wait_complete(struct tip_cygwin * priv, OVERLAPPED * o)
{
	DWORD sz;

	if (!GetOverlappedResult(priv->tc_h, o, &sz, TRUE)) return -1;

	return sz;
}

static int
ti_do_io(struct tip_cygwin * priv, void * buf, int len, OVERLAPPED * o, int wr)
{
	BOOL rc;
	DWORD sz;
	int err;

	/* setup overlapped */
	memset(o, 0, sizeof(*o));

	/* do io */
	if (wr)
		rc = WriteFile(priv->tc_h, buf, len, &sz, o);
	else
		rc = ReadFile(priv->tc_h, buf, len, &sz, o);

	/* done */
	if (rc) return sz;

	if ((err = GetLastError()) != ERROR_IO_PENDING) return -1;

	return 0; /* pending */
}

static int ti_do_io_lock(
	struct tip_cygwin * priv, void * buf, int len, OVERLAPPED * o, int wr)
{
	int rc;

	if (pthread_mutex_lock(&priv->tc_mtx)) return -1;

	rc = ti_do_io(priv, buf, len, o, wr);

	if (pthread_mutex_unlock(&priv->tc_mtx)) return -1;

	/* done */
	if (rc) return rc;

	return ti_wait_complete(priv, o);
}

static int ti_write_cygwin(struct tif * ti, void * buf, int len)
{
	struct tip_cygwin * priv = ti_priv(ti);
	OVERLAPPED o;

	return ti_do_io_lock(priv, buf, len, &o, 1);
}

static int ti_read_packet(struct tip_cygwin * priv, void * buf, int len)
{
	OVERLAPPED o;
	int rc;

	while (priv->tc_running)
	{
		rc = ti_do_io_lock(priv, buf, len, &o, 0);
		if (rc) return rc;
	}

	return -1;
}

static void * ti_reader(void * arg)
{
	struct tip_cygwin * priv = arg;
	unsigned char buf[2048];
	int len;

	while (priv->tc_running)
	{
		/* read a packet */
		if ((len = ti_read_packet(priv, buf, sizeof(buf))) == -1) break;

		assert(len > 0);

		/* write it's length */
		if (write(priv->tc_pipe[1], &len, sizeof(len)) != sizeof(len)) break;

		/* write payload */
		if (write(priv->tc_pipe[1], buf, len) != len) break;
	}

	priv->tc_running = -1;

	return NULL;
}

static struct tif * ti_open_cygwin(char * iface)
{
	struct tif * ti;
	struct tip_cygwin * priv;

	/* setup ti struct */
	ti = ti_alloc(sizeof(*priv));
	if (!ti) return NULL;
	priv = ti_priv(ti);

	ti->ti_name = ti_name_cygwin;
	ti->ti_set_mtu = ti_set_mtu_cygwin;
	ti->ti_close = ti_close_cygwin;
	ti->ti_fd = ti_fd_cygwin;
	ti->ti_read = ti_read_cygwin;
	ti->ti_write = ti_write_cygwin;
	ti->ti_set_mac = ti_set_mac_cygwin;
	ti->ti_set_ip = ti_set_ip_cygwin;

	/* setup iface */
	if (iface)
	{
		const ssize_t err
			= snprintf(priv->tc_guid, sizeof(priv->tc_guid), "%s", iface);
		if (err == -1 || (size_t) err >= sizeof(priv->tc_guid)) goto err;
	}
	if (ti_do_open_cygwin(priv) == -1) goto err;

	/* setup reader */
	if (pipe(priv->tc_pipe) == -1) goto err;

	if (pthread_mutex_init(&priv->tc_mtx, NULL)) goto err;

	/* launch reader */
	if (start_reader(priv)) goto err;

	return ti;
err:
	ti_do_free(ti);
	return NULL;
}

EXPORT struct tif * ti_open(char * iface) { return ti_open_cygwin(iface); }
