/**
 * WinPR: Windows Portable Runtime
 * Synchronization Functions
 *
 * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

#include <winpr/crt.h>
#include <winpr/file.h>
#include <winpr/sysinfo.h>

#include <winpr/synch.h>

#ifndef _WIN32
#include <errno.h>
#include <sys/time.h>
#include <signal.h>
#endif

#include "synch.h"

#ifndef _WIN32

#include "../handle/handle.h"

#include "../log.h"
#define TAG WINPR_TAG("synch.timer")

#ifdef WITH_POSIX_TIMER

static BOOL g_WaitableTimerSignalHandlerInstalled = FALSE;

void WaitableTimerSignalHandler(int signum, siginfo_t* siginfo, void* arg)
{
	WINPR_TIMER* timer = siginfo->si_value.sival_ptr;

	if (!timer || (signum != SIGALRM))
		return;

	if (timer->pfnCompletionRoutine)
	{
		timer->pfnCompletionRoutine(timer->lpArgToCompletionRoutine, 0, 0);

		if (timer->lPeriod)
		{
			timer->timeout.it_interval.tv_sec = (timer->lPeriod / 1000); /* seconds */
			timer->timeout.it_interval.tv_nsec = ((timer->lPeriod % 1000) * 1000000); /* nanoseconds */

			if ((timer_settime(timer->tid, 0, &(timer->timeout), NULL)) != 0)
			{
				WLog_ERR(TAG,"timer_settime");
			}
		}
	}
}

int InstallWaitableTimerSignalHandler()
{
	if (!g_WaitableTimerSignalHandlerInstalled)
	{
		struct sigaction action;
		sigemptyset(&action.sa_mask);
		sigaddset(&action.sa_mask, SIGALRM);
		action.sa_flags = SA_RESTART | SA_SIGINFO;
		action.sa_sigaction = (void*) &WaitableTimerSignalHandler;
		sigaction(SIGALRM, &action, NULL);
		g_WaitableTimerSignalHandlerInstalled = TRUE;
	}

	return 0;
}

#endif

int InitializeWaitableTimer(WINPR_TIMER* timer)
{
	if (!timer->lpArgToCompletionRoutine)
	{
#ifdef HAVE_TIMERFD_H
		int status;
		timer->fd = timerfd_create(CLOCK_MONOTONIC, 0);

		if (timer->fd <= 0)
		{
			free(timer);
			return -1;
		}

		status = fcntl(timer->fd, F_SETFL, O_NONBLOCK);

		if (status)
		{
			close(timer->fd);
			return -1;
		}

#endif
	}
	else
	{
#ifdef WITH_POSIX_TIMER
		struct sigevent sigev;
		InstallWaitableTimerSignalHandler();
		ZeroMemory(&sigev, sizeof(struct sigevent));
		sigev.sigev_notify = SIGEV_SIGNAL;
		sigev.sigev_signo = SIGALRM;
		sigev.sigev_value.sival_ptr = (void*) timer;

		if ((timer_create(CLOCK_MONOTONIC, &sigev, &(timer->tid))) != 0)
		{
			WLog_ERR(TAG,"timer_create");
			return -1;
		}

#endif
	}

	timer->bInit = TRUE;
	return 0;
}

/**
 * Waitable Timer
 */

HANDLE CreateWaitableTimerA(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCSTR lpTimerName)
{
	HANDLE handle = NULL;
	WINPR_TIMER* timer;
	timer = (WINPR_TIMER*) malloc(sizeof(WINPR_TIMER));

	if (timer)
	{
		WINPR_HANDLE_SET_TYPE(timer, HANDLE_TYPE_TIMER);
		handle = (HANDLE) timer;
		timer->fd = -1;
		timer->lPeriod = 0;
		timer->bManualReset = bManualReset;
		timer->pfnCompletionRoutine = NULL;
		timer->lpArgToCompletionRoutine = NULL;
		timer->bInit = FALSE;
	}

	return handle;
}

HANDLE CreateWaitableTimerW(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCWSTR lpTimerName)
{
	return NULL;
}

HANDLE CreateWaitableTimerExA(LPSECURITY_ATTRIBUTES lpTimerAttributes, LPCSTR lpTimerName, DWORD dwFlags, DWORD dwDesiredAccess)
{
	BOOL bManualReset;
	bManualReset = (dwFlags & CREATE_WAITABLE_TIMER_MANUAL_RESET) ? TRUE : FALSE;
	return CreateWaitableTimerA(lpTimerAttributes, bManualReset, lpTimerName);
}

HANDLE CreateWaitableTimerExW(LPSECURITY_ATTRIBUTES lpTimerAttributes, LPCWSTR lpTimerName, DWORD dwFlags, DWORD dwDesiredAccess)
{
	return NULL;
}

BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER* lpDueTime, LONG lPeriod,
					  PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume)
{
	ULONG Type;
	PVOID Object;
	WINPR_TIMER* timer;
#ifdef WITH_POSIX_TIMER
	LONGLONG seconds = 0;
	LONGLONG nanoseconds = 0;
#ifdef HAVE_TIMERFD_H
	int status = 0;
#endif /* HAVE_TIMERFD_H */
#endif /* WITH_POSIX_TIMER */

	if (!winpr_Handle_GetInfo(hTimer, &Type, &Object))
		return FALSE;

	if (Type != HANDLE_TYPE_TIMER)
		return FALSE;

	if (!lpDueTime)
		return FALSE;

	if (lPeriod < 0)
		return FALSE;

	timer = (WINPR_TIMER*) Object;
	timer->lPeriod = lPeriod; /* milliseconds */
	timer->pfnCompletionRoutine = pfnCompletionRoutine;
	timer->lpArgToCompletionRoutine = lpArgToCompletionRoutine;

	if (!timer->bInit)
	{
		if (InitializeWaitableTimer(timer) < 0)
			return FALSE;
	}

#ifdef WITH_POSIX_TIMER
	ZeroMemory(&(timer->timeout), sizeof(struct itimerspec));

	if (lpDueTime->QuadPart < 0)
	{
		LONGLONG due = lpDueTime->QuadPart * (-1);
		/* due time is in 100 nanosecond intervals */
		seconds = (due / 10000000);
		nanoseconds = ((due % 10000000) * 100);
	}
	else if (lpDueTime->QuadPart == 0)
	{
		seconds = nanoseconds = 0;
	}
	else
	{
		WLog_ERR(TAG, "absolute time not implemented");
		return FALSE;
	}

	if (lPeriod > 0)
	{
		timer->timeout.it_interval.tv_sec = (lPeriod / 1000); /* seconds */
		timer->timeout.it_interval.tv_nsec = ((lPeriod % 1000) * 1000000); /* nanoseconds */
	}

	if (lpDueTime->QuadPart != 0)
	{
		timer->timeout.it_value.tv_sec = seconds; /* seconds */
		timer->timeout.it_value.tv_nsec = nanoseconds; /* nanoseconds */
	}
	else
	{
		timer->timeout.it_value.tv_sec = timer->timeout.it_interval.tv_sec; /* seconds */
		timer->timeout.it_value.tv_nsec = timer->timeout.it_interval.tv_nsec; /* nanoseconds */
	}

	if (!timer->pfnCompletionRoutine)
	{
#ifdef HAVE_TIMERFD_H
		status = timerfd_settime(timer->fd, 0, &(timer->timeout), NULL);

		if (status)
		{
			WLog_ERR(TAG, "timerfd_settime failure: %d", status);
			return FALSE;
		}

#endif
	}
	else
	{
		if ((timer_settime(timer->tid, 0, &(timer->timeout), NULL)) != 0)
		{
			WLog_ERR(TAG,"timer_settime");
			return FALSE;
		}
	}

#endif
	return TRUE;
}

BOOL SetWaitableTimerEx(HANDLE hTimer, const LARGE_INTEGER* lpDueTime, LONG lPeriod,
						PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, PREASON_CONTEXT WakeContext, ULONG TolerableDelay)
{
	ULONG Type;
	PVOID Object;
	WINPR_TIMER* timer;

	if (!winpr_Handle_GetInfo(hTimer, &Type, &Object))
		return FALSE;

	if (Type == HANDLE_TYPE_TIMER)
	{
		timer = (WINPR_TIMER*) Object;
		return TRUE;
	}

	return TRUE;
}

HANDLE OpenWaitableTimerA(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCSTR lpTimerName)
{
	return NULL;
}

HANDLE OpenWaitableTimerW(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCWSTR lpTimerName)
{
	return NULL;
}

BOOL CancelWaitableTimer(HANDLE hTimer)
{
	return TRUE;
}

/**
 * Timer-Queue Timer
 */

/**
 * Design, Performance, and Optimization of Timer Strategies for Real-time ORBs:
 * http://www.cs.wustl.edu/~schmidt/Timer_Queue.html
 */

void timespec_add_ms(struct timespec* tspec, UINT32 ms)
{
	UINT64 ns = tspec->tv_nsec + (ms * 1000000);
	tspec->tv_sec += (ns / 1000000000);
	tspec->tv_nsec = (ns % 1000000000);
}

UINT64 timespec_to_ms(struct timespec* tspec)
{
	UINT64 ms;
	ms = tspec->tv_sec * 1000;
	ms += tspec->tv_nsec / 1000000;
	return ms;
}

static void timespec_gettimeofday(struct timespec* tspec)
{
	struct timeval tval;
	gettimeofday(&tval, NULL);
	tspec->tv_sec = tval.tv_sec;
	tspec->tv_nsec = tval.tv_usec * 1000;
}

static int timespec_compare(const struct timespec* tspec1, const struct timespec* tspec2)
{
	if (tspec1->tv_sec == tspec2->tv_sec)
		return (tspec1->tv_nsec - tspec2->tv_nsec);
	else
		return (tspec1->tv_sec - tspec2->tv_sec);
}

static void timespec_copy(struct timespec* dst, struct timespec* src)
{
	dst->tv_sec = src->tv_sec;
	dst->tv_nsec = src->tv_nsec;
}

void InsertTimerQueueTimer(WINPR_TIMER_QUEUE_TIMER** pHead, WINPR_TIMER_QUEUE_TIMER* timer)
{
	WINPR_TIMER_QUEUE_TIMER* node;

	if (!(*pHead))
	{
		*pHead = timer;
		timer->next = NULL;
		return;
	}

	node = *pHead;

	while (node->next)
	{
		if (timespec_compare(&(timer->ExpirationTime), &(node->ExpirationTime)) > 0)
		{
			if (timespec_compare(&(timer->ExpirationTime), &(node->next->ExpirationTime)) < 0)
				break;
		}

		node = node->next;
	}

	if (node->next)
	{
		timer->next = node->next->next;
		node->next = timer;
	}
	else
	{
		node->next = timer;
		timer->next = NULL;
	}
}

void RemoveTimerQueueTimer(WINPR_TIMER_QUEUE_TIMER** pHead, WINPR_TIMER_QUEUE_TIMER* timer)
{
	BOOL found = FALSE;
	WINPR_TIMER_QUEUE_TIMER* node;
	WINPR_TIMER_QUEUE_TIMER* prevNode;

	if (timer == *pHead)
	{
		*pHead = timer->next;
		timer->next = NULL;
		return;
	}

	node = *pHead;
	prevNode = NULL;

	while (node)
	{
		if (node == timer)
		{
			found = TRUE;
			break;
		}

		prevNode = node;
		node = node->next;
	}

	if (found)
	{
		if (prevNode)
		{
			prevNode->next = timer->next;
		}

		timer->next = NULL;
	}
}

int FireExpiredTimerQueueTimers(WINPR_TIMER_QUEUE* timerQueue)
{
	struct timespec CurrentTime;
	WINPR_TIMER_QUEUE_TIMER* node;

	if (!timerQueue->activeHead)
		return 0;

	timespec_gettimeofday(&CurrentTime);
	node = timerQueue->activeHead;

	while (node)
	{
		if (timespec_compare(&CurrentTime, &(node->ExpirationTime)) >= 0)
		{
			node->Callback(node->Parameter, TRUE);
			node->FireCount++;
			timerQueue->activeHead = node->next;
			node->next = NULL;

			if (node->Period)
			{
				timespec_add_ms(&(node->ExpirationTime), node->Period);
				InsertTimerQueueTimer(&(timerQueue->activeHead), node);
			}
			else
			{
				InsertTimerQueueTimer(&(timerQueue->inactiveHead), node);
			}

			node = timerQueue->activeHead;
		}
		else
		{
			break;
		}
	}

	return 0;
}

static void* TimerQueueThread(void* arg)
{
	int status;
	struct timespec timeout;
	WINPR_TIMER_QUEUE* timerQueue = (WINPR_TIMER_QUEUE*) arg;

	while (1)
	{
		pthread_mutex_lock(&(timerQueue->cond_mutex));
		timespec_gettimeofday(&timeout);

		if (!timerQueue->activeHead)
		{
			timespec_add_ms(&timeout, 50);
		}
		else
		{
			if (timespec_compare(&timeout, &(timerQueue->activeHead->ExpirationTime)) < 0)
			{
				timespec_copy(&timeout, &(timerQueue->activeHead->ExpirationTime));
			}
		}

		status = pthread_cond_timedwait(&(timerQueue->cond), &(timerQueue->cond_mutex), &timeout);
		FireExpiredTimerQueueTimers(timerQueue);
		pthread_mutex_unlock(&(timerQueue->cond_mutex));

		if (timerQueue->bCancelled)
			break;
	}

	return NULL;
}

int StartTimerQueueThread(WINPR_TIMER_QUEUE* timerQueue)
{
	pthread_cond_init(&(timerQueue->cond), NULL);
	pthread_mutex_init(&(timerQueue->cond_mutex), NULL);
	pthread_mutex_init(&(timerQueue->mutex), NULL);
	pthread_attr_init(&(timerQueue->attr));
	timerQueue->param.sched_priority = sched_get_priority_max(SCHED_FIFO);
	pthread_attr_setschedparam(&(timerQueue->attr), &(timerQueue->param));
	pthread_attr_setschedpolicy(&(timerQueue->attr), SCHED_FIFO);
	pthread_create(&(timerQueue->thread), &(timerQueue->attr), TimerQueueThread, timerQueue);
	return 0;
}

HANDLE CreateTimerQueue(void)
{
	HANDLE handle = NULL;
	WINPR_TIMER_QUEUE* timerQueue;
	timerQueue = (WINPR_TIMER_QUEUE*) malloc(sizeof(WINPR_TIMER_QUEUE));

	if (timerQueue)
	{
		ZeroMemory(timerQueue, sizeof(WINPR_TIMER_QUEUE));
		WINPR_HANDLE_SET_TYPE(timerQueue, HANDLE_TYPE_TIMER_QUEUE);
		handle = (HANDLE) timerQueue;
		timerQueue->activeHead = NULL;
		timerQueue->inactiveHead = NULL;
		timerQueue->bCancelled = FALSE;
		StartTimerQueueThread(timerQueue);
	}

	return handle;
}

BOOL DeleteTimerQueueEx(HANDLE TimerQueue, HANDLE CompletionEvent)
{
	void* rvalue;
	WINPR_TIMER_QUEUE* timerQueue;
	WINPR_TIMER_QUEUE_TIMER* node;
	WINPR_TIMER_QUEUE_TIMER* nextNode;

	if (!TimerQueue)
		return FALSE;

	timerQueue = (WINPR_TIMER_QUEUE*) TimerQueue;
	/* Cancel and delete timer queue timers */
	pthread_mutex_lock(&(timerQueue->cond_mutex));
	timerQueue->bCancelled = TRUE;
	pthread_cond_signal(&(timerQueue->cond));
	pthread_mutex_unlock(&(timerQueue->cond_mutex));
	pthread_join(timerQueue->thread, &rvalue);

	if (CompletionEvent == INVALID_HANDLE_VALUE)
	{
		/* Wait for all callback functions to complete before returning */
	}
	else
	{
		/* Cancel all timers and return immediately */
		/* Move all active timers to the inactive timer list */
		node = timerQueue->activeHead;

		while (node)
		{
			InsertTimerQueueTimer(&(timerQueue->inactiveHead), node);
			node = node->next;
		}

		timerQueue->activeHead = NULL;
		/* Once all timers are inactive, free them */
		node = timerQueue->inactiveHead;

		while (node)
		{
			nextNode = node->next;
			free(node);
			node = nextNode;
		}

		timerQueue->inactiveHead = NULL;
	}

	/* Delete timer queue */
	pthread_cond_destroy(&(timerQueue->cond));
	pthread_mutex_destroy(&(timerQueue->cond_mutex));
	pthread_mutex_destroy(&(timerQueue->mutex));
	pthread_attr_destroy(&(timerQueue->attr));
	free(timerQueue);

	if (CompletionEvent && (CompletionEvent != INVALID_HANDLE_VALUE))
		SetEvent(CompletionEvent);

	return TRUE;
}

BOOL DeleteTimerQueue(HANDLE TimerQueue)
{
	return DeleteTimerQueueEx(TimerQueue, NULL);
}

BOOL CreateTimerQueueTimer(PHANDLE phNewTimer, HANDLE TimerQueue,
						   WAITORTIMERCALLBACK Callback, PVOID Parameter, DWORD DueTime, DWORD Period, ULONG Flags)
{
	struct timespec CurrentTime;
	WINPR_TIMER_QUEUE* timerQueue;
	WINPR_TIMER_QUEUE_TIMER* timer;

	if (!TimerQueue)
		return FALSE;

	timespec_gettimeofday(&CurrentTime);
	timerQueue = (WINPR_TIMER_QUEUE*) TimerQueue;
	timer = (WINPR_TIMER_QUEUE_TIMER*) malloc(sizeof(WINPR_TIMER_QUEUE_TIMER));

	if (!timer)
		return FALSE;

	WINPR_HANDLE_SET_TYPE(timer, HANDLE_TYPE_TIMER_QUEUE_TIMER);
	*((UINT_PTR*) phNewTimer) = (UINT_PTR)(HANDLE) timer;
	timespec_copy(&(timer->StartTime), &CurrentTime);
	timespec_add_ms(&(timer->StartTime), DueTime);
	timespec_copy(&(timer->ExpirationTime), &(timer->StartTime));
	timer->Flags = Flags;
	timer->DueTime = DueTime;
	timer->Period = Period;
	timer->Callback = Callback;
	timer->Parameter = Parameter;
	timer->timerQueue = (WINPR_TIMER_QUEUE*) TimerQueue;
	timer->FireCount = 0;
	timer->next = NULL;
	pthread_mutex_lock(&(timerQueue->cond_mutex));
	InsertTimerQueueTimer(&(timerQueue->activeHead), timer);
	pthread_cond_signal(&(timerQueue->cond));
	pthread_mutex_unlock(&(timerQueue->cond_mutex));
	return TRUE;
}

BOOL ChangeTimerQueueTimer(HANDLE TimerQueue, HANDLE Timer, ULONG DueTime, ULONG Period)
{
	struct timespec CurrentTime;
	WINPR_TIMER_QUEUE* timerQueue;
	WINPR_TIMER_QUEUE_TIMER* timer;

	if (!TimerQueue || !Timer)
		return FALSE;

	timespec_gettimeofday(&CurrentTime);
	timerQueue = (WINPR_TIMER_QUEUE*) TimerQueue;
	timer = (WINPR_TIMER_QUEUE_TIMER*) Timer;
	pthread_mutex_lock(&(timerQueue->cond_mutex));
	RemoveTimerQueueTimer(&(timerQueue->activeHead), timer);
	RemoveTimerQueueTimer(&(timerQueue->inactiveHead), timer);
	timer->DueTime = DueTime;
	timer->Period = Period;
	timer->next = NULL;
	timespec_copy(&(timer->StartTime), &CurrentTime);
	timespec_add_ms(&(timer->StartTime), DueTime);
	timespec_copy(&(timer->ExpirationTime), &(timer->StartTime));
	InsertTimerQueueTimer(&(timerQueue->activeHead), timer);
	pthread_cond_signal(&(timerQueue->cond));
	pthread_mutex_unlock(&(timerQueue->cond_mutex));
	return TRUE;
}

BOOL DeleteTimerQueueTimer(HANDLE TimerQueue, HANDLE Timer, HANDLE CompletionEvent)
{
	WINPR_TIMER_QUEUE* timerQueue;
	WINPR_TIMER_QUEUE_TIMER* timer;

	if (!TimerQueue || !Timer)
		return FALSE;

	timerQueue = (WINPR_TIMER_QUEUE*) TimerQueue;
	timer = (WINPR_TIMER_QUEUE_TIMER*) Timer;
	pthread_mutex_lock(&(timerQueue->cond_mutex));

	if (CompletionEvent == INVALID_HANDLE_VALUE)
	{
		/* Wait for all callback functions to complete before returning */
	}
	else
	{
		/* Cancel timer and return immediately */
		RemoveTimerQueueTimer(&(timerQueue->activeHead), timer);
	}

	pthread_cond_signal(&(timerQueue->cond));
	pthread_mutex_unlock(&(timerQueue->cond_mutex));
	free(timer);

	if (CompletionEvent && (CompletionEvent != INVALID_HANDLE_VALUE))
		SetEvent(CompletionEvent);

	return TRUE;
}

#endif
