/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright 2015 Eion Robb and Pidgin Devs
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 */
 
 /* 
 /home/Eion/win32-dev/mingw/bin/gcc.exe -Wall -g -ggdb -O0 -pipe gnetworkmonitorwin32.c -shared -o gnetworkmonitorwin32.dll -static-libgcc -IC:/cygwin/home/Eion/win32-dev/gtk2-2.32/include/glib-2.0 -IC:/cygwin/home/Eion/win32-dev/gtk2-2.32/lib/glib-2.0/include -Wno-format  -LC:/cygwin/home/Eion/win32-dev/gtk2-2.32/lib -lglib-2.0 -lgio-2.0 -lgobject-2.0 -lws2_32 -Wno-unused-local-typedefs -ggdb -g 
 */

#ifdef NOT_A_MODULE
#include "config.h"
#endif

#include "gnetworkmonitorwin32.h"

#include <winsock2.h>
#include <mswsock.h>
#include <windows.h>
#include <nspapi.h>

#ifdef _MSC_VER
#	pragma comment(lib, "ws2_32")
#endif

static void g_network_monitor_win32_iface_init(GNetworkMonitorInterface *iface);
static void g_network_monitor_win32_initable_iface_init(GInitableIface *iface);

enum
{
	PROP_0,
	
	PROP_NETWORK_AVAILABLE,
	PROP_NETWORK_METERED,
	PROP_CONNECTIVITY
};

#if !GLIB_CHECK_VERSION(2, 44, 0)
typedef enum {
	G_NETWORK_CONNECTIVITY_LOCAL       = 1,
	G_NETWORK_CONNECTIVITY_LIMITED     = 2,
	G_NETWORK_CONNECTIVITY_PORTAL      = 3,
	G_NETWORK_CONNECTIVITY_FULL        = 4
} GNetworkConnectivity;

#define G_TYPE_NETWORK_CONNECTIVITY (g_network_connectivity_get_type ())
static GType g_network_connectivity_get_type(void)
{
	static volatile gsize g_define_type_id__volatile = 0;

	if (g_once_init_enter(&g_define_type_id__volatile))
	{
		static const GEnumValue values[] = {
			{ G_NETWORK_CONNECTIVITY_LOCAL, "G_NETWORK_CONNECTIVITY_LOCAL", "local" },
			{ G_NETWORK_CONNECTIVITY_LIMITED, "G_NETWORK_CONNECTIVITY_LIMITED", "limited" },
			{ G_NETWORK_CONNECTIVITY_PORTAL, "G_NETWORK_CONNECTIVITY_PORTAL", "portal" },
			{ G_NETWORK_CONNECTIVITY_FULL, "G_NETWORK_CONNECTIVITY_FULL", "full" },
			{ 0, NULL, NULL }
		};
		GType g_define_type_id = g_enum_register_static(g_intern_static_string("GNetworkConnectivity"), values);
		g_once_init_leave(&g_define_type_id__volatile, g_define_type_id);
	}

	return g_define_type_id__volatile;
}

#endif

#ifdef NOT_A_MODULE
G_DEFINE_TYPE_EXTENDED (GNetworkMonitorWin32, g_network_monitor_win32, G_TYPE_OBJECT, 0,
                        G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
                                               g_network_monitor_win32_initable_iface_init)
                        G_IMPLEMENT_INTERFACE(G_TYPE_NETWORK_MONITOR,
                                               g_network_monitor_win32_iface_init)
                        g_io_extension_point_implement(G_NETWORK_MONITOR_EXTENSION_POINT_NAME,
                                                       g_define_type_id,
                                                       "win32",
                                                       20))
#else
static void
g_network_monitor_win32_class_finalize (GNetworkMonitorWin32Class *klass)
{
}

G_DEFINE_DYNAMIC_TYPE_EXTENDED (GNetworkMonitorWin32, g_network_monitor_win32, G_TYPE_OBJECT, 0,
                                G_IMPLEMENT_INTERFACE_DYNAMIC(G_TYPE_INITABLE,
                                                       g_network_monitor_win32_initable_iface_init)
                                G_IMPLEMENT_INTERFACE_DYNAMIC(G_TYPE_NETWORK_MONITOR,
                                                       g_network_monitor_win32_iface_init))


void
g_io_module_load(GIOModule *module)
{
	g_network_monitor_win32_register_type(G_TYPE_MODULE(module));
	g_io_extension_point_implement(G_NETWORK_MONITOR_EXTENSION_POINT_NAME,
                                   G_TYPE_NETWORK_MONITOR_WIN32,
                                   "win32",
                                   20);
}

void
g_io_module_unload(GIOModule *module)
{
	
}

gchar **
g_io_module_query(void)
{
	gchar *eps[] = {
		G_NETWORK_MONITOR_EXTENSION_POINT_NAME,
		NULL
	};
	
	return g_strdupv(eps);
}

#endif

static gint _get_connected_network_count(gboolean *);
static gpointer _network_change_thread(gpointer data);
static gboolean _network_change_thread_cb(gpointer data);

static guint network_changed_signal = 0;
		
struct _GNetworkMonitorWin32Private
{
	int current_network_count;

	GThread *thread;
	/* Mutex for the other global vars */
	GMutex mutex;
	gboolean network_initialized;
	HANDLE network_change_handle;
	boolean has_internet;
};

static void
g_network_monitor_win32_init(GNetworkMonitorWin32 *monitor)
{
	WORD wVersionRequested;
	WSADATA wsaData;
	
	wVersionRequested = MAKEWORD(2, 2);
	WSAStartup(wVersionRequested, &wsaData);
	
	monitor->priv = G_TYPE_INSTANCE_GET_PRIVATE(monitor,
                                           G_TYPE_NETWORK_MONITOR_WIN32,
                                           GNetworkMonitorWin32Private);
}


static gboolean
g_network_monitor_win32_initable_init(GInitable     *initable,
                                      GCancellable  *cancellable,
                                      GError       **error)
{
	gboolean has_internet;
	GNetworkMonitorWin32 *monitor = G_NETWORK_MONITOR_WIN32(initable);
	gint cnt = _get_connected_network_count(&has_internet);

	monitor->priv->network_initialized = TRUE;
	monitor->priv->network_change_handle = NULL;
	g_mutex_init(&monitor->priv->mutex);
	
	if (cnt < 0) {
		// Assume there is a network
		monitor->priv->current_network_count = 1;
		monitor->priv->has_internet = TRUE;
		// Don't listen for network changes if we can't tell anyway
	} else {
		monitor->priv->current_network_count = cnt;
		monitor->priv->has_internet = has_internet;
		
		monitor->priv->thread = g_thread_try_new("Win32NetworkMonitor", _network_change_thread, monitor, error);
		if (monitor->priv->thread == NULL) {
			g_error("Could not spawn win32 network monitor thread");
			return FALSE;
		}
	}
	
	return TRUE;
}

static void
g_network_monitor_win32_get_property(GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
	GNetworkMonitorWin32 *monitor = G_NETWORK_MONITOR_WIN32(object);

	switch (prop_id)
	{
		case PROP_NETWORK_AVAILABLE:
			g_value_set_boolean(value, (monitor->priv->current_network_count > 0));
			break;

		case PROP_NETWORK_METERED:
			g_value_set_boolean(value, FALSE);
			break;
		
		case PROP_CONNECTIVITY:
			g_value_set_enum(value,
							 monitor->priv->has_internet ?
							 G_NETWORK_CONNECTIVITY_FULL :
							 ((monitor->priv->current_network_count > 0) ?
							 G_NETWORK_CONNECTIVITY_LIMITED :
							 G_NETWORK_CONNECTIVITY_LOCAL));
			break;
		
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
	}

}

static gboolean
g_network_monitor_win32_can_reach(GNetworkMonitor      *parent,
                                  GSocketConnectable   *connectable,
                                  GCancellable         *cancellable,
                                  GError              **error)
{
	GNetworkMonitorWin32 *monitor = G_NETWORK_MONITOR_WIN32(parent);

	return (monitor->priv->current_network_count > 0);
}

static gint
_get_connected_network_count(gboolean *has_internet)
{
	gint net_cnt = 0;

	WSAQUERYSET qs;
	HANDLE h;
	gint retval;
	int errorid;
	
	if (has_internet != NULL) {
		*has_internet = FALSE;
	}

	memset(&qs, 0, sizeof(WSAQUERYSET));
	qs.dwSize = sizeof(WSAQUERYSET);
	qs.dwNameSpace = NS_NLA;

	retval = WSALookupServiceBeginA(&qs, LUP_RETURN_ALL, &h);
	if (retval != ERROR_SUCCESS) {
		gchar *msg;
		errorid = WSAGetLastError();
		msg = g_win32_error_message(errorid);
		g_error("NetworkMonitorWin32: Couldn't retrieve NLA SP lookup handle. "
		        "NLA service is probably not running. Message: %s (%d).",
		        msg, errorid);
		g_free(msg);

		return -1;
	} else {
		char buf[4096];
		WSAQUERYSET *res = (LPWSAQUERYSET) buf;
		DWORD size = sizeof(buf);
		while ((retval = WSALookupServiceNextA(h, LUP_RETURN_ALL, &size, res)) == ERROR_SUCCESS) {
			net_cnt++;
			g_message("NetworkMonitorWin32: found network '%s'",
					  res->lpszServiceInstanceName && *res->lpszServiceInstanceName ? res->lpszServiceInstanceName : 
					  (res->lpszComment && *res->lpszComment ? res->lpszComment : "(NULL)"));
			if (has_internet != NULL && res->lpBlob != NULL) {
				DWORD bloboffset = 0;
				PNLA_BLOB info;
				do {
					info = (PNLA_BLOB) &(res->lpBlob->pBlobData[bloboffset]);
					if (info->header.type == NLA_CONNECTIVITY) {
						if (info->data.connectivity.internet == NLA_INTERNET_YES) {
							*has_internet = TRUE;
						}
						
						g_message("NetworkMonitorWin32: has internet connectivity? '%s'",
							info->data.connectivity.internet == NLA_INTERNET_YES ? "Yes" : (
							info->data.connectivity.internet == NLA_INTERNET_NO ? "No" : "Unknown"
							));
						
						break;
					}
					bloboffset = info->header.nextOffset;
				} while (bloboffset != 0);
			}
			size = sizeof(buf);
		}

		errorid = WSAGetLastError();
		if (!(errorid == WSA_E_NO_MORE || errorid == WSAENOMORE)) {
			gchar *msg = g_win32_error_message(errorid);
			g_warning("NetworkMonitorWin32: got unexpected NLA response %s (%d)", msg, errorid);
			g_free(msg);

			net_cnt = -1;
		}

		retval = WSALookupServiceEnd(h);
	}

	return net_cnt;

}

static gboolean
_network_change_thread_cb(gpointer data)
{
	GNetworkMonitorWin32 *monitor = data;
	gint new_count;
	gboolean has_internet;

	new_count = _get_connected_network_count(&has_internet);

	if (new_count < 0)
		return FALSE;

	g_message("Received Network Change Notification. Current network count is %d, previous count was %d.", new_count, monitor->priv->current_network_count);
	
	monitor->priv->has_internet = has_internet;
	
    if (monitor->priv->current_network_count != new_count)
	{
		monitor->priv->current_network_count = new_count;
		g_object_notify(G_OBJECT(monitor), "network-available");
	}

    g_signal_emit(monitor, network_changed_signal, 0, (new_count > 0));

	return FALSE;
}

static gboolean _print_debug_msg(gpointer data) {
	gchar *msg = data;
	g_debug("%s", msg);
	g_free(msg);
	return FALSE;
}

static gpointer
_network_change_thread(gpointer data)
{
	GNetworkMonitorWin32 *monitor = data;
	WSAQUERYSET qs;
	WSAEVENT *nla_event;
	time_t last_trigger = time(NULL) - 31;
	char buf[4096];
	WSAQUERYSET *res = (LPWSAQUERYSET) buf;
	DWORD size;

	if ((nla_event = WSACreateEvent()) == WSA_INVALID_EVENT) {
		int errorid = WSAGetLastError();
		gchar *msg = g_win32_error_message(errorid);
		g_timeout_add(0, _print_debug_msg,
						   g_strdup_printf("Couldn't create WSA event. "
										   "Message: %s (%d).", msg, errorid));
		g_free(msg);
		g_thread_exit(NULL);
		return NULL;
	}

	while (TRUE) {
		int retval;
		DWORD retLen = 0;
		WSACOMPLETION completion;
		WSAOVERLAPPED overlapped;

		g_mutex_lock(&monitor->priv->mutex);
		if (monitor->priv->network_initialized == FALSE) {
			/* purple_network_uninit has been called */
			WSACloseEvent(nla_event);
			g_mutex_unlock(&monitor->priv->mutex);
			g_thread_exit(NULL);
			return NULL;
		}

		if (monitor->priv->network_change_handle == NULL) {
			memset(&qs, 0, sizeof(WSAQUERYSET));
			qs.dwSize = sizeof(WSAQUERYSET);
			qs.dwNameSpace = NS_NLA;
			if (WSALookupServiceBegin(&qs, 0, &monitor->priv->network_change_handle) == SOCKET_ERROR) {
				int errorid = WSAGetLastError();
				gchar *msg = g_win32_error_message(errorid);
				g_timeout_add(0, _print_debug_msg,
								   g_strdup_printf("Couldn't retrieve NLA SP lookup handle. "
												   "NLA service is probably not running. Message: %s (%d).",
													msg, errorid));
				g_free(msg);
				WSACloseEvent(nla_event);
				g_mutex_unlock(&monitor->priv->mutex);
				g_thread_exit(NULL);
				return NULL;
			}
		}
		g_mutex_unlock(&monitor->priv->mutex);

		memset(&completion, 0, sizeof(WSACOMPLETION));
		completion.Type = NSP_NOTIFY_EVENT;
		overlapped.hEvent = nla_event;
		completion.Parameters.Event.lpOverlapped = &overlapped;

		if (WSANSPIoctl(monitor->priv->network_change_handle, SIO_NSP_NOTIFY_CHANGE, NULL, 0, NULL, 0, &retLen, &completion) == SOCKET_ERROR) {
			int errorid = WSAGetLastError();
			if (errorid == WSA_INVALID_HANDLE) {
				g_timeout_add(0, _print_debug_msg,
								   g_strdup("Invalid NLA handle; resetting."));
				g_mutex_lock(&monitor->priv->mutex);
				retval = WSALookupServiceEnd(monitor->priv->network_change_handle);
				monitor->priv->network_change_handle = NULL;
				g_mutex_unlock(&monitor->priv->mutex);
				continue;
			/* WSA_IO_PENDING indicates successful async notification will happen */
			} else if (errorid != WSA_IO_PENDING) {
				gchar *msg = g_win32_error_message(errorid);
				g_timeout_add(0, _print_debug_msg,
								   g_strdup_printf("Unable to wait for changes. Message: %s (%d).",
												   msg, errorid));
				g_free(msg);
			}
		}

		/* Make sure at least 30 seconds have elapsed since the last
		 * notification so we don't peg the cpu if this keeps changing. */
		if ((time(NULL) - last_trigger) < 30)
			Sleep(30000);

		/* This will block until NLA notifies us */
		retval = WaitForSingleObjectEx(nla_event, WSA_INFINITE, TRUE);

		last_trigger = time(NULL);

		g_mutex_lock(&monitor->priv->mutex);
		if (monitor->priv->network_initialized == FALSE) {
			/* Time to die */
			WSACloseEvent(nla_event);
			g_mutex_unlock(&monitor->priv->mutex);
			g_thread_exit(NULL);
			return NULL;
		}

		size = sizeof(buf);
		while ((retval = WSALookupServiceNext(monitor->priv->network_change_handle, 0, &size, res)) == ERROR_SUCCESS) {
			/*g_timeout_add(0, _print_debug_msg,
							   g_strdup_printf("thread found network '%s'",
											   res->lpszServiceInstanceName ? res->lpszServiceInstanceName : "(NULL)"));*/
			size = sizeof(buf);
		}

		WSAResetEvent(nla_event);
		g_mutex_unlock(&monitor->priv->mutex);

		g_timeout_add(0, _network_change_thread_cb, monitor);
	}

	g_thread_exit(NULL);
	return NULL;
}

static void
g_network_monitor_win32_finalize(GObject *object)
{
	GNetworkMonitorWin32 *monitor = G_NETWORK_MONITOR_WIN32(object);

	g_mutex_lock(&monitor->priv->mutex);
	monitor->priv->network_initialized = FALSE;
	if (monitor->priv->network_change_handle != NULL) {
		int retval;
		/* Trigger the NLA thread to stop waiting for network changes. Not
		 * doing this can cause hangs on WSACleanup. */
		g_warning("NetworkMonitorWin32: Terminating the NLA thread");
		if ((retval = WSALookupServiceEnd(monitor->priv->network_change_handle)) == SOCKET_ERROR) {
			int errorid = WSAGetLastError();
			gchar *msg = g_win32_error_message(errorid);
			g_error("NetworkMonitorWin32: Unable to kill NLA thread. Message: %s (%d).",
				msg, errorid);
			g_free(msg);
		}
		monitor->priv->network_change_handle = NULL;

	}
	g_mutex_unlock(&monitor->priv->mutex);

	G_OBJECT_CLASS(g_network_monitor_win32_parent_class)->finalize (object);
}

static void
g_network_monitor_win32_class_init(GNetworkMonitorWin32Class *monitor_class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(monitor_class);

	g_type_class_add_private(monitor_class, sizeof(GNetworkMonitorWin32Private));

	gobject_class->get_property = g_network_monitor_win32_get_property;
	gobject_class->finalize = g_network_monitor_win32_finalize;
	
	g_object_class_override_property(gobject_class, PROP_NETWORK_AVAILABLE, "network-available");
	g_object_class_override_property(gobject_class, PROP_CONNECTIVITY, "connectivity");
	g_object_class_override_property(gobject_class, PROP_NETWORK_METERED, "network-metered");
}

static void
g_network_monitor_win32_iface_init(GNetworkMonitorInterface *monitor_iface)
{
	monitor_iface->can_reach = g_network_monitor_win32_can_reach;
	
	network_changed_signal = g_signal_lookup("network-changed", G_TYPE_NETWORK_MONITOR);
	
	if (glib_check_version(2, 46, 0)) {
		g_object_interface_install_property(monitor_iface,
                                            g_param_spec_boolean ("network-metered",
                                                                  "Network metered",
                                                                  "Whether the network is metered",
                                                                  FALSE,
                                                                  G_PARAM_READABLE |
                                                                  G_PARAM_STATIC_STRINGS));
	}
	if (glib_check_version(2, 44, 0)) {
		g_object_interface_install_property(monitor_iface,
                                            g_param_spec_enum ("connectivity",
                                                               "Network connectivity",
                                                               "Level of network connectivity",
                                                               G_TYPE_NETWORK_CONNECTIVITY,
                                                               G_NETWORK_CONNECTIVITY_FULL,
                                                               G_PARAM_READABLE |
                                                               G_PARAM_STATIC_STRINGS));
	}
}

static void
g_network_monitor_win32_initable_iface_init(GInitableIface *iface)
{
	iface->init = g_network_monitor_win32_initable_init;
}
