/*
 * Copyright (C) 2013 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include <library.h>
#include <daemon.h>

#include <utils/backtrace.h>
#include <threading/thread.h>

/**
 * The name of our service, both internal and external
 */
#define SERVICE_NAME "charon-svc"

/**
 * Current service status
 */
static SERVICE_STATUS status;

/**
 * Handle for service status
 */
static SERVICE_STATUS_HANDLE handle;

/**
 * Wait event for main thread
 */
static HANDLE event;

/**
 * hook in library for debugging messages
 */
extern void (*dbg) (debug_t group, level_t level, char *fmt, ...);

/**
 * Forward declaration
 */
static DWORD WINAPI service_handler(DWORD dwControl, DWORD dwEventType,
									LPVOID lpEventData, LPVOID lpContext);

/**
 * Logging hook for library logs, using stderr output
 */
static void dbg_stderr(debug_t group, level_t level, char *fmt, ...)
{
	va_list args;

	if (level <= 1)
	{
		va_start(args, fmt);
		fprintf(stderr, "00[%N] ", debug_names, group);
		vfprintf(stderr, fmt, args);
		fprintf(stderr, "\n");
		va_end(args);
	}
}

/**
 * Log strongSwan/Windows version during startup
 */
static void print_version()
{
	OSVERSIONINFOEX osvie;

	memset(&osvie, 0, sizeof(osvie));
	osvie.dwOSVersionInfoSize = sizeof(osvie);

	if (GetVersionEx((LPOSVERSIONINFO)&osvie))
	{
		DBG1(DBG_DMN, "Starting IKE service %s (strongSwan %s, "
			 "Windows %s %d.%d.%d (SP %d.%d))", SERVICE_NAME, VERSION,
			 osvie.wProductType == VER_NT_WORKSTATION ? "Client" : "Server",
			 osvie.dwMajorVersion, osvie.dwMinorVersion, osvie.dwBuildNumber,
			 osvie.wServicePackMajor, osvie.wServicePackMinor);
	}
}

/**
 * Update service state to SCM, increase check point if state didn't change
 */
static void update_status(DWORD state)
{
	if (state == status.dwCurrentState)
	{
		status.dwCheckPoint++;
	}
	else
	{
		status.dwCheckPoint = 0;
	}
	status.dwCurrentState = state;
	if (handle)
	{
		SetServiceStatus(handle, &status);
	}
}

/**
 * Control handler for console
 */
static BOOL WINAPI console_handler(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
		case CTRL_C_EVENT:
		case CTRL_BREAK_EVENT:
		case CTRL_CLOSE_EVENT:
			DBG1(DBG_DMN, "application is stopping, cleaning up");
			if (status.dwCurrentState == SERVICE_RUNNING)
			{
				charon->bus->alert(charon->bus, ALERT_SHUTDOWN_SIGNAL,
								   dwCtrlType);
			}
			/* signal main thread to clean up */
			SetEvent(event);
			return TRUE;
		default:
			return FALSE;
	}
}

/**
 * Service handler function
 */
static DWORD WINAPI service_handler(DWORD dwControl, DWORD dwEventType,
									LPVOID lpEventData, LPVOID lpContext)
{
	switch (dwControl)
	{
		case SERVICE_CONTROL_STOP:
		case SERVICE_CONTROL_SHUTDOWN:
			DBG1(DBG_DMN, "service is stopping, cleaning up");
			if (status.dwCurrentState == SERVICE_RUNNING)
			{
				charon->bus->alert(charon->bus, ALERT_SHUTDOWN_SIGNAL,
								   dwControl);
			}
			/* signal main thread to clean up */
			SetEvent(event);
			return NO_ERROR;
		case SERVICE_CONTROL_INTERROGATE:
			return NO_ERROR;
		default:
			return ERROR_CALL_NOT_IMPLEMENTED;
	}
}

/**
 * Wait for console program shutdown
 */
static int console_wait()
{
	update_status(SERVICE_RUNNING);

	if (WaitForSingleObjectEx(event, INFINITE, TRUE) != WAIT_OBJECT_0)
	{
		return 2;
	}
	return 0;
}

/**
 * Wait for service shutdown
 */
static int service_wait()
{
	/* service is initialized, we now accept control requests */
	status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
	update_status(SERVICE_RUNNING);
	status.dwControlsAccepted = 0;

	if (WaitForSingleObjectEx(event, INFINITE, TRUE) != WAIT_OBJECT_0)
	{
		return 2;
	}
	return 0;
}

/**
 * Add namespace alias
 */
static void __attribute__ ((constructor))register_namespace()
{
	/* inherit settings from charon */
	library_add_namespace("charon");
}

/**
 * Initialize and run charon using a wait function
 */
static void init_and_run(DWORD dwArgc, LPTSTR *lpszArgv, int (*wait)())
{
	level_t levels[DBG_MAX];
	int i;

	for (i = 0; i < DBG_MAX; i++)
	{
		levels[i] = LEVEL_CTRL;
	}

	update_status(SERVICE_START_PENDING);
	event = CreateEvent(NULL, FALSE, FALSE, NULL);
	if (event)
	{
		update_status(SERVICE_START_PENDING);
		if (library_init(NULL, SERVICE_NAME))
		{
			update_status(SERVICE_START_PENDING);
			if (libcharon_init())
			{
				charon->set_default_loggers(charon, levels, TRUE);
				charon->load_loggers(charon);
				print_version();
				update_status(SERVICE_START_PENDING);
				if (charon->initialize(charon, PLUGINS))
				{
					update_status(SERVICE_START_PENDING);
					lib->plugins->status(lib->plugins, LEVEL_CTRL);

					charon->start(charon);

					status.dwWin32ExitCode = wait();
				}
				update_status(SERVICE_STOP_PENDING);
				libcharon_deinit();
			}
			update_status(SERVICE_STOP_PENDING);
			library_deinit();
		}
		update_status(SERVICE_STOP_PENDING);
		CloseHandle(event);
	}
	update_status(SERVICE_STOPPED);
}

/**
 * Main routine when running from console
 */
static void console_main(DWORD dwArgc, LPTSTR *lpszArgv)
{
	status.dwWin32ExitCode = 1;

	if (SetConsoleCtrlHandler(console_handler, TRUE))
	{
		init_and_run(dwArgc, lpszArgv, console_wait);
		SetConsoleCtrlHandler(console_handler, FALSE);
	}
}

/**
 * Switch the working directory to the executable directory
 */
static bool switch_workingdir()
{
	CHAR path[MAX_PATH], *pos;
	HMODULE module;

	module = GetModuleHandle(NULL);
	if (!module)
	{
		return FALSE;
	}
	if (!GetModuleFileName(module, path, sizeof(path)))
	{
		return FALSE;
	}
	pos = strrchr(path, '\\');
	if (!pos)
	{
		return FALSE;
	}
	*pos = 0;
	return SetCurrentDirectory(path);
}

/**
 * Service main routine when running as service
 */
static void WINAPI service_main(DWORD dwArgc, LPTSTR *lpszArgv)
{
	memset(&status, 0, sizeof(status));
	status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	status.dwWin32ExitCode = 1;

	handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, service_handler, NULL);
	if (handle)
	{
		if (switch_workingdir())
		{
			init_and_run(dwArgc, lpszArgv, service_wait);
		}
	}
}

/**
 * Main function, starts the service
 */
int main(int argc, char *argv[])
{
	SERVICE_TABLE_ENTRY services[] = {
		{
			.lpServiceName = SERVICE_NAME,
			.lpServiceProc = service_main,
		},
		{ NULL, NULL },
	};
	DWORD err;

	dbg = dbg_stderr;

	if (!StartServiceCtrlDispatcher(services))
	{
		err = GetLastError();
		if (err == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)
		{
			console_main(argc, argv);
		}
		else
		{
			return 2;
		}
	}
	return status.dwWin32ExitCode;
}
