/*
 * notify.c: Notification implementation
 *
 * Copyright (C) 2017 Frank Morgner <frankmorgner@gmail.com>
 *
 * 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.1 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

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

#include "notify.h"

#if defined(ENABLE_NOTIFY) && (defined(__APPLE__))

#include "libopensc/internal.h"
#include "libopensc/log.h"
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static pid_t child = -1;

void sc_notify_init(void)
{
}

void sc_notify_close(void)
{
	if (child > 0) {
		int i, status;
		for (i = 0; child != waitpid(child, &status, WNOHANG); i++) {
			switch (i) {
				case 0:
					kill(child, SIGKILL);
					break;
				case 1:
					kill(child, SIGTERM);
					break;
				default:
					/* SIGTERM was our last resort */
					return;
			}
			usleep(100);
		}
		child = -1;
	}
}

#endif

#if defined(ENABLE_NOTIFY) && defined(_WIN32)

#include "common/compat_strlcpy.h"
#include "invisible_window.h"
#include <shellapi.h>
#include <shlwapi.h>

// {83C35893-99C6-4600-BFDB-45925C53BDD9}
static const GUID myGUID = { 0x83c35893, 0x99c6, 0x4600, { 0xbf, 0xdb, 0x45, 0x92, 0x5c, 0x53, 0xbd, 0xd9 } };
HINSTANCE sc_notify_instance = NULL;
HWND sc_notify_hwnd = NULL;
BOOL delete_icon = TRUE;
#define IDI_SMARTCARD       102
#define WMAPP_NOTIFYCALLBACK (WM_APP + 1)
static BOOL RestoreTooltip();
void ShowContextMenu(HWND hwnd, POINT pt);

// we need commctrl v6 for LoadIconMetric()
#include <commctrl.h>

static int GetMonitorScalingRatio(void)
{
	POINT pt = { 0, 0 };
	HMONITOR monitor;
	MONITORINFOEX info = { .cbSize = sizeof(MONITORINFOEX) };
	DEVMODE devmode = { .dmSize = sizeof(DEVMODE) };
	monitor = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
	if (!monitor)
		goto err;
	if (!GetMonitorInfo(monitor, (LPMONITORINFO)&info))
		goto err;
	if (!EnumDisplaySettings(info.szDevice, ENUM_CURRENT_SETTINGS, &devmode))
		goto err;
	if (info.rcMonitor.left == info.rcMonitor.right)
		goto err;
	return 100*devmode.dmPelsWidth/(info.rcMonitor.right - info.rcMonitor.left);
err:
	return 100;
}

static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) {
		case WM_COMMAND:
			{
				int const wmId = LOWORD(wParam);
				// Parse the menu selection:
				switch (wmId)
				{
					case WMAPP_EXIT:
						break;

					default:
						return DefWindowProc(hwnd, message, wParam, lParam);
				}
			}
			break;

		case WMAPP_NOTIFYCALLBACK:
			switch (LOWORD(lParam)) {
				case NIN_BALLOONTIMEOUT:
				case NIN_BALLOONUSERCLICK:
					RestoreTooltip();
					break;

				case WM_CONTEXTMENU:
					{
						int scaling = GetMonitorScalingRatio();
						POINT const pt = { 100*LOWORD(wParam)/scaling, 100*HIWORD(wParam)/scaling };
						ShowContextMenu(hwnd, pt);
					}
					break;
			}
			break;

		default:
			return DefWindowProc(hwnd, message, wParam, lParam);
	}

    return 0;
}

static const char* lpszClassName = "NOTIFY_CLASS";

static BOOL AddNotificationIcon(void)
{
	NOTIFYICONDATA nid = {0};
	TCHAR path[MAX_PATH]={0};
	BOOL r;

	sc_notify_hwnd = create_invisible_window(lpszClassName, WndProc,
			sc_notify_instance);
	if (!sc_notify_hwnd) {
		return FALSE;
	}

	nid.cbSize = sizeof(NOTIFYICONDATA);
	nid.hWnd = sc_notify_hwnd;
	// add the icon, setting the icon, tooltip, and callback message.
	// the icon will be identified with the GUID
	nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE | NIF_SHOWTIP | NIF_GUID;
	nid.guidItem = myGUID; 
	nid.uCallbackMessage = WMAPP_NOTIFYCALLBACK;
	LoadIconMetric(sc_notify_instance, MAKEINTRESOURCEW(IDI_SMARTCARD), LIM_SMALL, &nid.hIcon);
	if (GetModuleFileName(NULL, path, ARRAYSIZE(path))) {
		const char *basename = strrchr(path, '\\');
		if (basename) {
			basename++;
			if (0 != strcmp(basename, "opensc-notify.exe")) {
				/* Allow creation of system tray icon only for
				 * "opensc-notify.exe" to avoid creation of the same icon by
				 * multiple processes. */
				delete_icon = FALSE;
				return FALSE;
			}
		}
		strlcpy(nid.szTip, path, ARRAYSIZE(nid.szTip));
	} else {
		strcpy(nid.szTip, PACKAGE_NAME);
	}

	r = Shell_NotifyIcon(NIM_ADD, &nid);
	if (TRUE == r) {
		nid.uVersion = NOTIFYICON_VERSION_4;
		r = Shell_NotifyIcon(NIM_SETVERSION, &nid);
	}

	return r;
}

static BOOL DeleteNotificationIcon(void)
{
	BOOL r;
	NOTIFYICONDATA nid = {0};

	if (!delete_icon) {
		return FALSE;
	}

	nid.cbSize = sizeof(NOTIFYICONDATA);
	nid.uFlags = NIF_GUID;
	nid.guidItem = myGUID; 

	r  = Shell_NotifyIcon(NIM_DELETE, &nid);

	r &= delete_invisible_window(sc_notify_hwnd, lpszClassName,
			sc_notify_instance);
	sc_notify_hwnd = NULL;

	return r;
}

static BOOL RestoreTooltip()
{
    // After the balloon is dismissed, restore the tooltip.
	NOTIFYICONDATA nid = {0};

	nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.uFlags = NIF_SHOWTIP | NIF_GUID;
	nid.guidItem = myGUID; 

    return Shell_NotifyIcon(NIM_MODIFY, &nid);
}

void ShowContextMenu(HWND hwnd, POINT pt)
{
	HMENU hMenu;
	hMenu=CreatePopupMenu();

    if (hMenu) {
		AppendMenu(hMenu, MF_STRING, WMAPP_EXIT, ui_get_str(NULL, NULL, NULL, NOTIFY_EXIT));

		// our window must be foreground before calling TrackPopupMenu or the menu will not disappear when the user clicks away
		SetForegroundWindow(hwnd);

		// respect menu drop alignment
		UINT uFlags = TPM_RIGHTBUTTON;
		if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0) {
			uFlags |= TPM_RIGHTALIGN;
		} else {
			uFlags |= TPM_LEFTALIGN;
		}

		TrackPopupMenuEx(hMenu, uFlags, pt.x, pt.y, hwnd, NULL);
	}
}


static void notify_shell(struct sc_context *ctx,
		const char *title, const char *text, LPCTSTR icon_path, int icon_index)
{
	NOTIFYICONDATA nid = {0};
	HICON icon = NULL;

	nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.uFlags = NIF_GUID;
    nid.guidItem = myGUID;

	if (title) {
		strlcpy(nid.szInfoTitle, title, ARRAYSIZE(nid.szInfoTitle));
	}
	if (text) {
		nid.uFlags |= NIF_INFO;
		strlcpy(nid.szInfo, text, ARRAYSIZE(nid.szInfo));
	}
	if (icon_path) {
		ExtractIconEx(icon_path, icon_index, &icon, NULL, 1);
		if (icon) {
			nid.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON;
			nid.hBalloonIcon = icon;
		}
	}

    Shell_NotifyIcon(NIM_MODIFY, &nid);

	if (icon) {
		DestroyIcon(icon);
	}
}

void sc_notify_init(void)
{
	if (!sc_notify_instance) {
		/* returns the HINSTANCE of the exe. If the code executes in a DLL,
		 * sc_notify_instance_notify should be pre-initialized */
		sc_notify_instance = GetModuleHandle(NULL);
	}
	AddNotificationIcon();
}

void sc_notify_close(void)
{
	DeleteNotificationIcon();
}

void sc_notify(const char *title, const char *text)
{
	notify_shell(NULL, title, text, NULL, 0);
}

void sc_notify_id(struct sc_context *ctx, struct sc_atr *atr,
		struct sc_pkcs15_card *p15card, enum ui_str id)
{
	const char *title, *text;
	LPCTSTR icon_path = NULL;
	int icon_index = 0;
	title = ui_get_str(ctx, atr, p15card, id);
	text = ui_get_str(ctx, atr, p15card, id+1);

	switch (id) {
		case NOTIFY_CARD_INSERTED:
			icon_path = TEXT("%SYSTEMROOT%\\system32\\SCardDlg.dll");
			icon_index = 3;
			break;
		case NOTIFY_CARD_REMOVED:
			icon_path = TEXT("%SYSTEMROOT%\\system32\\SCardDlg.dll");
			icon_index = 2;
			break;
		case NOTIFY_PIN_GOOD:
			icon_path = TEXT("%SYSTEMROOT%\\system32\\certmgr.dll");
			icon_index = 16;
			break;
		case NOTIFY_PIN_BAD:
			icon_path = TEXT("%SYSTEMROOT%\\system32\\certmgr.dll");
			icon_index = 11;
			break;
		default:
			break;
	}

	notify_shell(ctx, title, text, icon_path, icon_index);
}

#elif defined(ENABLE_NOTIFY) && defined(__APPLE__)

static void notify_proxy(struct sc_context *ctx,
		const char *title, const char* subtitle,
		const char *text, const char *icon, const char *sound,
		const char *group)
{
	/* terminal-notifier does not reliably activate keychain when clicked on
	 * the notification
	 * (https://github.com/julienXX/terminal-notifier/issues/196), that's why
	 * we're including NotificationProxy which has similar features */
	const char notificationproxy[] = "/Library/OpenSC/Applications/NotificationProxy.app/Contents/MacOS/NotificationProxy";

	if (ctx && ctx->app_name
			&& (0 == strcmp(ctx->app_name, "opensc-pkcs11")
				|| 0 == strcmp(ctx->app_name, "onepin-opensc-pkcs11"))) {
		/* some programs don't like forking when loading our PKCS#11 module,
		 * see https://github.com/OpenSC/OpenSC/issues/1174.
		 * TODO implementing an XPC service which sends the notification should
		 * work, though. See
		 * https://github.com/OpenSC/OpenSC/issues/1304#issuecomment-376656003 */
		return;
	}

	if (child > 0) {
		int status;
		if (0 == waitpid(child, &status, WNOHANG)) {
			kill(child, SIGKILL);
			usleep(100);
			if (0 == waitpid(child, &status, WNOHANG)) {
				sc_log(ctx, "Can't kill %ld, skipping current notification", (long) child);
				return;
			}
		}
	}

	child = fork();
	switch (child) {
		case 0:
			/* child process */

			/* for some reason the user _tokend can call brew's installation of
			 * terminal-notifier, but it cannot call `/usr/bin/open` with
			 * NotificationProxy.app that we're shipping... However, while
			 * `sudo -u _tokend /usr/local/bin/terminal-notifier -title test`
			 * works in the terminal, it hangs when executed from the tokend
			 * process.  For now, we try to deliver the notification anyway
			 * making sure that we are waiting for only one forked process. */
			if (0 > execl(notificationproxy, notificationproxy,
						title ? title : "",
						subtitle ? subtitle : "",
						text ? text : "",
						icon ? icon : "",
						group ? group : "",
						sound ? sound : "",
						(char *) NULL)) {
				perror("exec failed");
				exit(0);
			}
			break;
		case -1:
			sc_log(ctx, "failed to fork for notification");
			break;
		default:
			if (ctx) {
				sc_log(ctx, "Created %ld for notification:", (long) child);
				sc_log(ctx, "%s %s %s %s %s %s %s", notificationproxy,
						title ? title : "",
						subtitle ? subtitle : "",
						text ? text : "",
						icon ? icon : "",
						group ? group : "",
						sound ? sound : "");
			}
			break;
	}
}

void sc_notify(const char *title, const char *text)
{
	notify_proxy(NULL, title, NULL, text, NULL, NULL, NULL);
}

void sc_notify_id(struct sc_context *ctx, struct sc_atr *atr,
		struct sc_pkcs15_card *p15card, enum ui_str id)
{
	const char *title, *text, *icon, *group;
	title = ui_get_str(ctx, atr, p15card, id);
	text = ui_get_str(ctx, atr, p15card, id+1);

	if (p15card && p15card->card && p15card->card->reader) {
		group = p15card->card->reader->name;
	} else {
		group = ctx ? ctx->app_name : NULL;
	}

	switch (id) {
		case NOTIFY_CARD_INSERTED:
			icon = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/VCard.icns";
			break;
		case NOTIFY_CARD_REMOVED:
			icon = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/EjectMediaIcon.icns";
			break;
		case NOTIFY_PIN_GOOD:
			icon = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/UnlockedIcon.icns";
			break;
		case NOTIFY_PIN_BAD:
			icon = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/LockedIcon.icns";
			break;
		default:
			icon = NULL;
			break;
	}

	notify_proxy(ctx, title, NULL, text, icon, NULL, group);
}

#elif defined(ENABLE_NOTIFY) && defined(ENABLE_GIO2)

#include <gio/gio.h>
#include "libopensc/log.h"

static GApplication *application = NULL;

void sc_notify_init(void)
{
	if (!application) {
		application = g_application_new("org.opensc.notify", G_APPLICATION_NON_UNIQUE);
	}
	if (application && FALSE == g_application_get_is_registered(application)) {
		g_application_register(application, NULL, NULL);
	}
}

void sc_notify_close(void)
{
	if (application) {
		g_object_unref(application);
		application = NULL;
	}
}

static void notify_gio(struct sc_context *ctx,
		const char *title, const char *text, const char *icon,
		const char *group)
{
	if (application
			&& g_application_get_is_registered(application)
			&& g_application_get_dbus_connection(application)) {
		GIcon *gicon = NULL;
		GNotification *notification = g_notification_new(title);
		if (!notification) {
			return;
		}

		if (text) {
			g_notification_set_body(notification, text);
		}
		if (icon) {
			gicon = g_themed_icon_new(icon);
			if (gicon) {
				g_notification_set_icon(notification, gicon);
			}
		}

		if (ctx) {
			sc_log(ctx, "%s %s %s %s",
					title ? title : "",
					text ? text : "",
					icon ? icon : "",
					group ? group : "");
		}

		g_application_send_notification(application, group, notification);

		if (gicon) {
			g_object_unref(gicon);
		}
		g_object_unref(notification);
	}
}

#else

void sc_notify_init(void) {}
void sc_notify_close(void) {}
void sc_notify(const char *title, const char *text) {}
void sc_notify_id(struct sc_context *ctx, struct sc_atr *atr,
		struct sc_pkcs15_card *p15card, enum ui_str id) {}

#endif

#if defined(ENABLE_NOTIFY) && defined(ENABLE_GIO2)
void sc_notify(const char *title, const char *text)
{
	notify_gio(NULL, title, text, NULL, NULL);
}

void sc_notify_id(struct sc_context *ctx, struct sc_atr *atr,
		struct sc_pkcs15_card *p15card, enum ui_str id)
{
	const char *title, *text, *icon, *group;
	title = ui_get_str(ctx, atr, p15card, id);
	text = ui_get_str(ctx, atr, p15card, id+1);

	if (p15card && p15card->card && p15card->card->reader) {
		group = p15card->card->reader->name;
	} else {
		group = ctx ? ctx->app_name : NULL;
	}

	switch (id) {
		case NOTIFY_CARD_INSERTED:
			icon = "contact-new";
			break;
		case NOTIFY_CARD_REMOVED:
			icon = "media-eject";
			break;
		case NOTIFY_PIN_GOOD:
			icon = "changes-allow";
			break;
		case NOTIFY_PIN_BAD:
			icon = "changes-prevent";
			break;
		default:
			icon = NULL;
			break;
	}

	notify_gio(ctx, title, text, icon, group);
}

#endif
