/*
 * nt_ppsimpl.c - PPS API client implementation
 *
 * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
 * The contents of 'html/copyright.html' apply.
 * ----------------------------------------------------------------------
 * Most of this code is from the the original 'timepps.h' for windows
 * where these functions where coded as 'inline'. While this was perhaps
 * a convenient thing to do, using this amount of code as 'static inline'
 * functions is generally a Bad Idea (tm).
 *
 * Not to mention that there are some static variables that got duplicated
 * into the various modules...
 */

#include <config.h>

#include <stddef.h>	/* offsetof() */
#include <io.h>		/* _get_osfhandle() */

#include "timepps.h"
#include "ntp_stdlib.h"
#include "ntp_iocpltypes.h"
#include "ntp_iocplmem.h"

struct InstListNode {
	struct InstListNode *	next;
	pps_handle_t		ppsu;
	DevCtx_t *		devu;
};
typedef struct ProvListNode ProvListNode_t;

static struct InstListNode *	g_active_units;
static ppsapi_provider *	g_provider_list;
static ppsapi_provider *	g_curr_provider;

static void
ppsu_register(
	pps_handle_t ppsu,
	DevCtx_t *   devu)
{
	struct InstListNode *	node;

	if (devu && (node = IOCPLPoolAlloc(sizeof(*node), "PPS registration"))) {
		node->next = g_active_units;
		node->ppsu = ppsu;
		node->devu = DevCtxAttach(devu);
		devu->pps_active = TRUE;
		g_active_units = node;
	}
}

static void
ppsu_remove(
	pps_handle_t ppsu)
{
	struct InstListNode **	link;
	struct InstListNode *	node;

	link = &g_active_units;
	while (NULL != (node = *link)) {
		if (node->ppsu == ppsu) {
			node->devu->pps_active = FALSE;
			DevCtxDetach(node->devu);
			*link = node->next;
			IOCPLPoolFree(node, "PPS registration");
		} else {
			link = &node->next;
		}
	}
}

static HKEY
myRegOpenKey(
	const char * szSubKey)
{
	static const char * const s_RegKey =
		"SYSTEM\\CurrentControlSet\\services\\NTP";

	HKEY	hkey1 = NULL;
	HKEY	hkey2 = NULL;
	DWORD	rc;

	rc = RegOpenKeyExA(HKEY_LOCAL_MACHINE, s_RegKey, 0, KEY_READ, &hkey1);
	if (ERROR_SUCCESS  != rc)
		return NULL;
	if (!(szSubKey && *szSubKey))
		return hkey1;

	rc = RegOpenKeyExA(hkey1, szSubKey, 0, KEY_READ, &hkey2);
	RegCloseKey(hkey1);
	if (ERROR_SUCCESS != rc)
		return NULL;
	return hkey2;
}

static char*
myRegReadMultiString(
	HKEY        hKey   ,
	const char *szValue,
	DWORD      *pSize  )
{
	char *	endp;
	char *	retv  = NULL;
	DWORD	rSize = 0, rType = REG_NONE, rc;

	/* take two turns: one to get the size, another one to malloc & read */
	do {
		if (rType != REG_NONE) {
			retv = emalloc(rSize += 2);
		}
		rc = RegQueryValueExA(hKey, szValue, NULL, &rType, retv, &rSize);
		if (ERROR_SUCCESS != rc || (REG_SZ != rType && REG_MULTI_SZ != rType))
			goto fail;	
	} while (NULL == retv);

	/* trim trailing NULs and ensure two of them */
	endp = retv + rSize;
	while (endp != retv && endp[-1])
		--endp;
	if (endp != retv) {
		endp[0] = endp[1] = '\0';
		if (NULL != pSize)
			*pSize = (DWORD)(endp - retv);
		return retv;
	}
fail:
	free(retv);
	if (NULL != pSize)
		*pSize = 0;
	return NULL;
}

static DWORD
myRegReadDWord(
	HKEY        hKey   ,
	const char *szValue,
	DWORD       Default)
{
	DWORD	rc, rSize, rType, rValue;
	
	rSize = sizeof(rValue);
	rc = RegQueryValueExA(hKey, szValue, NULL, &rType, (PBYTE)&rValue, &rSize);
	if (rc != ERROR_SUCCESS || rSize != sizeof(rValue) || rType != REG_DWORD)
		rValue = Default;
	return rValue;
}


static pps_handle_t
internal_create_pps_handle(
	void *	prov_context
	)
{
	pps_unit_t *	punit = NULL;

	if (NULL == g_curr_provider) {
		fprintf(stderr, "create_pps_handle: provider backend called me outside time_pps_create\n");
		return (pps_handle_t)NULL;
	}

	punit = emalloc(sizeof(*punit));
	punit->provider = g_curr_provider;
	punit->context = prov_context;
	punit->magic = PPSAPI_MAGIC_UNIT;

	return (pps_handle_t)punit;
}

static pps_unit_t *
unit_from_ppsapi_handle(
	pps_handle_t	handle
	)
{
	pps_unit_t *punit = (pps_unit_t *)handle;
	if (!(punit && PPSAPI_MAGIC_UNIT == punit->magic))
		punit = NULL;
	return punit;
}

/* ntpd on Windows only looks to errno after finding 'GetLastError()'
 * returns NO_ERROR.  To accomodate its use of msyslog in portable code
 * such as refclock_atom.c, this implementation always clears the
 * Windows error code using 'SetLastError(NO_ERROR)' when returning an
 * errno.  This is also a good idea for any non-ntpd clients as they
 * should rely only the errno for PPSAPI functions.
 */
static int
set_pps_errno(
	int	e
)
{
	SetLastError(NO_ERROR);
	errno = e;
	return -1;
}


/*Format a Windows errro into a temporary buffer from buffer lib */
static char *
fmt_err(
	DWORD ec
	)
{
	char * buff = NULL;
	char * endp = NULL;

	/* get buffer & format message, ensure termination */
	LIB_GETBUF(buff);
	FormatMessageA(
		FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		ec,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		buff, LIB_BUFLENGTH,
		NULL);
	buff[LIB_BUFLENGTH - 1] = '\0';
	/* strip trailing whitespace & CR/LF stuff & the trailing dot */
	endp = buff + strlen(buff);
	while (endp != buff && endp[-1] <= ' ')
		--endp;
	if (endp != buff && endp[-1] == '.')
		--endp;
	*endp = '\0';
	/* If windows fails to format this, resort to numeric output... */
	if (!*buff) {
		snprintf(buff, LIB_BUFLENGTH,
			 "(unknown windows error %lu / 0x%08lx)",
			 (unsigned long)ec, (unsigned long)ec);
	}
	/* That's it for now... */
	return buff;
}

/* Cleanup & error return for actual loading steps */
static int
cleanup_load(
	ppsapi_provider *	prov,
	HMODULE			hmod,
	int			errc
)
{
	/* if possible, decref the library handle */
	if (NULL != hmod)
		FreeLibrary(hmod);
	/* if possible, drop our provider structure */
	if (prov) {
		free(prov->short_name);
		free(prov->full_name);
		free(prov);
	}
	/* error code pass-through */
	return errc;
}

/* Get the directory path (!!) of the calling process. We want the true
 * 8bit character version, and we should not trust '_pgmptr' or
 * '_get_pgmptr()' too much. There's still some doubt around
 * 'GetModuleFileNameA()', but all in all this seems to be the most
 * reliable solution, even if it's a PITA with the buffer sizes.
 */
static char*
get_module_path(void)
{
	static const size_t s_smax = 4096;

	char *	buff = NULL;
	char *	lsep;
	DWORD	slen, nlen;

	for (slen = 128; slen <= s_smax; slen <<= 1) {
		buff = realloc(buff, slen);
		if (NULL == buff)
			goto fail;
		nlen = GetModuleFileNameA(NULL, buff, slen);
		if (0 == nlen)
			goto fail;
		if (nlen < slen)
			break;
	}
	if (slen > s_smax)
		goto fail;
	lsep = strrchr(buff, '\\');
	if (NULL == lsep)
		goto fail;
	*++lsep = '\0';
	return realloc(buff, (size_t)(lsep - buff) + 1);

fail:
	free(buff);
	return NULL;
}

static char *
get_provider_list(void)
{
	static char * s_Value = NULL;

	HKEY hKey;
	DWORD rSize;
	char *cp, *op;

	if (s_Value != NULL)
		return (*s_Value) ? s_Value : NULL;

	/*
	** try registry first
	*/
	hKey = myRegOpenKey(NULL);
	if (NULL == hKey)
		goto regfail;

	s_Value = myRegReadMultiString(hKey, "PPSProviders", &rSize);
	if (NULL == s_Value)
		goto regfail;

	/* make sure we have backslashes in the path */
	for (cp = s_Value; rSize; --rSize, ++cp)
		if (*cp == '/')
			*cp = '\\';
regfail:
	if (NULL != hKey)
		RegCloseKey(hKey);

	if (s_Value && *s_Value)
		return s_Value;

	/*
	** try environment next.
	*/
	free(s_Value);
	s_Value = NULL;

	/* try to get env var */
	cp = getenv("PPSAPI_DLLS");
	if (!(cp && *cp))
		goto envfail;

	/* get size & allocate buffer */
	rSize = strlen(cp);
	s_Value = emalloc(rSize + 2);

	/* copy string value and convert to MULTI_SZ.
	 * Converts sequences of ';' to a single NUL byte, and replaces
	 * slashes by backslashes on the fly.
	 */
	for (op = s_Value; *cp; ++cp) {
		if (*cp == '/') {
			*op++ = '\\';
		} else if (*cp == ';') {
			if (op != s_Value)
				*op++ = '\0';
		} else {
			*op++ = *cp;
		}
	}
	
	*op++ = '\0';
	*op = '\0';
	return s_Value;

envfail:
	free(s_Value);
	s_Value = calloc(2, 1);
	return s_Value;
}


/* Iteration helper for the provider list. Naked names (without *any*
 * path) will be prepended with path to the executable running this
 * code. While this was not necessary until Win7, newer versions of
 * Windows seem to have tighter restrictions from where to load code, at
 * least as long as the binary is not signed.
 */
static char*
provlist_next_item(
	const char ** iter
	)
{
	static char *	s_modpath    /* = NULL */;
	static char	s_nullstr[1] /* = { '\0' } */;

	const char     *phead, *phold;
	char	       *retv, *endp;
	int/*BOOL*/	nodir;
	size_t		slen, mlen;

	/* get next item -- might be start of a new round or the end */
again:
	if (*iter == NULL)
		*iter = phead = get_provider_list();
	else
		*iter = phead = *iter + strlen(*iter) + 1;
	if (!(phead && *phead)) {
		*iter = NULL;
		return NULL;
	}

	/* Inspect the next section of input string. It must be
	 * either an absolute path or just a name.
	 */
	if (isalpha(phead[0]) && phead[1] == ':' && phead[2] == '\\') {
		nodir = FALSE;
	} else {
		nodir = TRUE;
		phold = phead;
		while (NULL != (endp = strpbrk(phold, "\\:")))
			phold = endp + 1;
		if (phead != phold) {
			msyslog(LOG_WARNING,
				"pps api: path component(s) of '%s' ignored, use '%s'",
				phead, phold);
			phead = phold;
		}
	}
	if (!*phead || strchr("\\.:", phead[strlen(phead) - 1]))
		goto again;	/* empty or looks like a directory! */

	/* Make sure we have a proper module path when we need one. */
	if (nodir && NULL == s_modpath) {
		s_modpath = get_module_path();
		if (NULL == s_modpath)
			s_modpath = s_nullstr;
	}

	/* Prepare buffer for copy of file name. */
	slen = strlen(phead);
	if (nodir && NULL != s_modpath) {
		/* Prepend full path to executable to the name. */
		mlen = strlen(s_modpath);
		endp = retv = emalloc(mlen + slen + 1);
		memcpy(endp, s_modpath, mlen);
		endp += mlen;
	} else {
		endp = retv = emalloc(slen + 1);
	}
	memcpy(endp, phead, slen);
	endp[slen] = '\0';

	return retv;
}


/* Try to load & init a provider DLL. (NOT a device instance!) */ 
static int
load_pps_provider(
	const char *	dllpath
	)
{
	static const char	msgfmt[] = "load_pps_provider: '%s': %s";

	char			short_name[16];
	char			full_name[64];
	ppsapi_provider *	prov;
	HMODULE			hmod;
	pppsapi_prov_init	pprov_init;

	prov = emalloc_zero(sizeof(*prov));
	hmod = LoadLibraryA(dllpath);
	if (NULL == hmod) {
		msyslog(LOG_WARNING, msgfmt, dllpath,
			fmt_err(GetLastError()));
		return cleanup_load(prov, hmod, ENOENT);
	}

	pprov_init = (pppsapi_prov_init)GetProcAddress(hmod, "ppsapi_prov_init");
	if (NULL == pprov_init) {
		msyslog(LOG_WARNING, msgfmt, dllpath,
			"main entry point not found");
		return cleanup_load(prov, hmod, EFAULT);
	}

	prov->caps = (*pprov_init)(PPSAPI_TIMEPPS_PROV_VER,
		&internal_create_pps_handle,
		&pps_ntp_timestamp_from_counter,
		short_name, sizeof(short_name),
		full_name, sizeof(full_name));
	if (!prov->caps) {
		msyslog(LOG_WARNING, msgfmt, dllpath,
			"no capabilities");
		return cleanup_load(prov, hmod, EACCES);
	}
	
	prov->short_name = (*short_name) ? estrdup(short_name) : NULL;
	prov->full_name  = (*full_name ) ? estrdup(full_name ) : NULL;
	if (NULL == prov->short_name || NULL == prov->full_name) {
		msyslog(LOG_WARNING, msgfmt, dllpath,
			"missing names");
		return cleanup_load(prov, hmod, EINVAL);
	}

	prov->ptime_pps_create = (provtime_pps_create)
		GetProcAddress(hmod, "prov_time_pps_create");
	prov->ptime_pps_destroy = (provtime_pps_destroy)
		GetProcAddress(hmod, "prov_time_pps_destroy");
	prov->ptime_pps_setparams = (provtime_pps_setparams)
		GetProcAddress(hmod, "prov_time_pps_setparams");
	prov->ptime_pps_fetch = (provtime_pps_fetch)
		GetProcAddress(hmod, "prov_time_pps_fetch");
	prov->ptime_pps_kcbind = (provtime_pps_kcbind)
		GetProcAddress(hmod, "prov_time_pps_kcbind");

	if (NULL == prov->ptime_pps_create    ||
	    NULL == prov->ptime_pps_destroy   ||
	    NULL == prov->ptime_pps_setparams ||
	    NULL == prov->ptime_pps_fetch     ||
	    NULL == prov->ptime_pps_kcbind     )
	{
		msyslog(LOG_WARNING, msgfmt, prov->short_name,
			"missing entry point");
		return cleanup_load(prov, hmod, EINVAL);
	}

	prov->next = g_provider_list;
	g_provider_list = prov;

	return 0;
}


static ppsapi_provider*
get_first_provider(void)
{
	const char *	 itpos;
	char *		 dll;
	ppsapi_provider *prov, *hold;
	int		 err;

	/* check if we have done our work so far... */
	if (g_provider_list == INVALID_HANDLE_VALUE)
		return NULL;
	if (g_provider_list != NULL)
		return g_provider_list;

	itpos = NULL;
	while (NULL != (dll = provlist_next_item(&itpos))) {
		err = load_pps_provider(dll);
		if (err)
			msyslog(LOG_ERR, "time_pps_create: load failed (%s) --> %d / %s",
				dll, err, strerror(err));
		else
			msyslog(LOG_INFO, "time_pps_create: loaded '%s'",
				dll);
		free(dll);
	}

	/* reverse the list, possibly mark as EMPTY */
	prov = g_provider_list;
	if (NULL != prov) {
		g_provider_list = NULL;
		do {
			hold = prov;
			prov = hold->next;
			hold->next = g_provider_list;
			g_provider_list = hold;
		} while (prov);
		prov = g_provider_list;
	} else {
		g_provider_list = INVALID_HANDLE_VALUE;
	}
	return prov;
}

int
time_pps_create(
	int		filedes,/* device file descriptor */
	pps_handle_t *	phandle	/* returned handle */
	)
{
	HANDLE			winhandle;
	ppsapi_provider *	prov;
	pps_handle_t		ppshandle;
	int			err;

	if (NULL == phandle)
		return set_pps_errno(EFAULT);

	winhandle = (HANDLE)_get_osfhandle(filedes);
	if (INVALID_HANDLE_VALUE == winhandle)
		return set_pps_errno(EBADF);

	/* Hand off to each provider in turn until one returns a PPS
	 * handle or they've all declined.
	 *
	 * [Bug 3139] Since we potentially tried a series of DLLs, it's
	 * a good question what the returned error should be if all of
	 * them failed; Returning the error from the last attempt is as
	 * good as any but for single DLL (which is the normal case)
	 * this provides slightly more information.
	 */
	err = ENOEXEC;
	prov = get_first_provider();
	if (NULL == prov) {
		msyslog(LOG_ERR, "time_pps_create: %s",
			"no providers available");
		return set_pps_errno(err);
	} else do {
		ppshandle = 0;
		g_curr_provider = prov;
		err = (*prov->ptime_pps_create)(winhandle, &ppshandle);
		g_curr_provider = NULL;
		if (!err && ppshandle) {
			*phandle = ppshandle;
			ppsu_register(ppshandle, serial_devctx(winhandle));
			return 0;
		}
		msyslog(LOG_INFO, "time_pps_create: provider '%s' failed: %d / %s",
			prov->short_name, err, strerror(err));
	} while (NULL != (prov = prov->next));

	msyslog(LOG_ERR, "time_pps_create: %s",
		"all providers failed");
	return set_pps_errno(err);
}


int
time_pps_destroy(
	pps_handle_t handle
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);
	int		err   = 0;

	/* Check for valid arguments */
	if (NULL == punit)
		return set_pps_errno(EBADF);
	/* Call provider. Note the handle is gone anyway... */
	ppsu_remove(handle);
	err = (*punit->provider->ptime_pps_destroy)(punit, punit->context);
	free(punit);
	if (err)
		return set_pps_errno(err);
	return 0;
}


int
time_pps_setparams(
	pps_handle_t handle,
	const pps_params_t *params
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);
	int		err   = 0;

	/* Check for valid arguments */
	if (NULL == punit)
		return set_pps_errno(EBADF);
	if (NULL == params)
		return set_pps_errno(EFAULT);
	/* Call provider */
	err = (*punit->provider->ptime_pps_setparams)(punit, punit->context, params);
	if (err)
		return set_pps_errno(err);
	return 0;
}


int
time_pps_getparams(
	pps_handle_t	handle,
	pps_params_t *	params_buf
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);

	/* Check for valid arguments */
	punit;
	if (NULL == punit)
		return set_pps_errno(EBADF);
	if (NULL == params_buf)
		return set_pps_errno(EFAULT);
	/* Copy out parameters */
	*params_buf = punit->params;
	return 0;
}


int
time_pps_getcap(
	pps_handle_t	handle,
	int *		pmode
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);

	/* Check for valid arguments */
	if (NULL == punit)
		return set_pps_errno(EBADF);
	if (NULL == pmode)
		return set_pps_errno(EFAULT);
	/* Copy out capabilities */
		*pmode = punit->provider->caps;
	return 0;
}


int
time_pps_fetch(
	pps_handle_t		handle,
	const int		tsformat,
	pps_info_t *		pinfo,
	const struct timespec *	ptimeout
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);
	int		err   = 0;

	/* Check for valid arguments */
	if (NULL == punit)
		return set_pps_errno(EBADF);
	if (NULL == pinfo)
		return set_pps_errno(EFAULT);
	/* Fetch timestamps */
	err = (*punit->provider->ptime_pps_fetch)(punit,
		punit->context,
		tsformat,
		pinfo,
		ptimeout);

	if (err)
		return set_pps_errno(err);
	return 0;
}


int
time_pps_kcbind(
	pps_handle_t handle,
	const int kernel_consumer,
	const int edge, const int tsformat
	)
{
	pps_unit_t *	punit = unit_from_ppsapi_handle(handle);
	int		err   = 0;

	/* Check for valid arguments */
	if (NULL == punit)
		return set_pps_errno(EBADF);
	/* Call provider */
	err = (*punit->provider->ptime_pps_kcbind)(
		punit,
		punit->context,
		kernel_consumer,
		edge,
		tsformat);

	if (err)
		return set_pps_errno(err);
	return 0;
}

/* -*- that's all folks! -*- */
