/* ----------------------------------------------------------------------- *
 *
 *  alarm.c - alarm queue handling module.
 *
 *   Copyright 2006 Ian Kent <raven@themaw.net> - All Rights Reserved
 *
 *   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, Inc., 675 Mass Ave, Cambridge MA 02139,
 *   USA; either version 2 of the License, or (at your option) any later
 *   version; incorporated herein by reference.
 *
 * ----------------------------------------------------------------------- */

#include <stdlib.h>
#include "automount.h"

struct alarm {
	time_t time;
	unsigned int cancel;
	struct autofs_point *ap;
	struct list_head list;
};

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond;
static LIST_HEAD(alarms);

#define alarm_lock() \
do { \
	int _alm_lock = pthread_mutex_lock(&mutex); \
	if (_alm_lock) \
		fatal(_alm_lock); \
} while (0)

#define alarm_unlock() \
do { \
	int _alm_unlock = pthread_mutex_unlock(&mutex); \
	if (_alm_unlock) \
		fatal(_alm_unlock); \
} while (0)

/* Insert alarm entry on ordered list. */
int alarm_add(struct autofs_point *ap, time_t seconds)
{
	struct list_head *head;
	struct list_head *p;
	struct alarm *new;
	time_t now = monotonic_time(NULL);
	time_t next_alarm = 0;
	unsigned int empty = 1;
	int status;

	if (!seconds)
		return 1;

	new = malloc(sizeof(struct alarm));
	if (!new)
		return 0;

	new->ap = ap;
	new->cancel = 0;
	new->time = now + seconds;

	alarm_lock();

	head = &alarms;

	/* Check if we have a pending alarm */
	if (!list_empty(head)) {
		struct alarm *current;
		current = list_entry(head->next, struct alarm, list);
		next_alarm = current->time;
		empty = 0;
	}

	list_for_each(p, head) {
		struct alarm *this;

		this = list_entry(p, struct alarm, list);
		if (this->time >= new->time) {
			list_add_tail(&new->list, p);
			break;
		}
	}
	if (p == head)
		list_add_tail(&new->list, p);

	/*
	 * Wake the alarm thread if it is not busy (ie. if the
	 * alarms list was empty) or if the new alarm comes before
	 * the alarm we are currently waiting on.
	 */
	if (empty || new->time < next_alarm) {
        	status = pthread_cond_signal(&cond);
		if (status)
			fatal(status);
	}

	alarm_unlock();

	return 1;
}

void alarm_delete(struct autofs_point *ap)
{
	struct list_head *head;
	struct list_head *p;
	struct alarm *current;
	unsigned int signal_cancel = 0;
	int status;

	alarm_lock();

	head = &alarms;

	if (list_empty(head)) {
		alarm_unlock();
		return;
	}

	current = list_entry(head->next, struct alarm, list);

	p = head->next;
	while (p != head) {
		struct alarm *this;

		this = list_entry(p, struct alarm, list);
		p = p->next;

		if (ap == this->ap) {
			if (current != this) {
				list_del_init(&this->list);
				free(this);
				continue;
			}
			/* Mark as canceled */
			this->cancel = 1;
			this->time = 0;
			signal_cancel = 1;
		}
	}

	if (signal_cancel) {
        	status = pthread_cond_signal(&cond);
		if (status)
			fatal(status);
	}

	alarm_unlock();

	return;
}

static void *alarm_handler(void *arg)
{
	struct list_head *head;
	struct timespec expire;
	struct alarm *first;
	time_t now;
	int status;

	alarm_lock();

	head = &alarms;

	while (1) {
		if (list_empty(head)) {
			/* No alarms, wait for one to be added */
			status = pthread_cond_wait(&cond, &mutex);
			if (status)
				fatal(status);
			continue;
		}

		first = list_entry(head->next, struct alarm, list);

		now = monotonic_time(NULL);

		if (first->time > now) {
			struct timespec nsecs;

			/* 
			 * Wait for alarm to trigger or a new alarm 
			 * to be added.
			 */
			clock_gettime(CLOCK_MONOTONIC, &nsecs);
			expire.tv_sec = first->time;
			expire.tv_nsec = nsecs.tv_nsec;

			status = pthread_cond_timedwait(&cond, &mutex, &expire);
			if (status && status != ETIMEDOUT)
				fatal(status);
		} else {
			/* First alarm has triggered, run it */

			list_del(&first->list);

			if (!first->cancel) {
				struct autofs_point *ap = first->ap;
				alarm_unlock(); 
				st_add_task(ap, ST_EXPIRE);
				alarm_lock();
			}
			free(first);
		}
	}
	/* Will never come here, so alarm_unlock is not necessary */
}

int alarm_start_handler(void)
{
	pthread_t thid;
	pthread_attr_t attrs;
	pthread_attr_t *pattrs = &attrs;
	pthread_condattr_t condattrs;
	int status;

	status = pthread_attr_init(pattrs);
	if (status)
		pattrs = NULL;
	else {
		pthread_attr_setdetachstate(pattrs, PTHREAD_CREATE_DETACHED);
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
		pthread_attr_setstacksize(pattrs, PTHREAD_STACK_MIN*4);
#endif
	}

	status = pthread_condattr_init(&condattrs);
	if (status)
		fatal(status);

	status = pthread_condattr_setclock(&condattrs, CLOCK_MONOTONIC);
	if (status)
		fatal(status);

	status = pthread_cond_init(&cond, &condattrs);
	if (status)
		fatal(status);

	status = pthread_create(&thid, pattrs, alarm_handler, NULL);

	pthread_condattr_destroy(&condattrs);

	if (pattrs)
		pthread_attr_destroy(pattrs);

	return !status;
}

