/* -*- mode: C; c-file-style: "linux"; indent-tabs-mode: t -*-
 * gdatetime-source.c - copy&paste from https://bugzilla.gnome.org/show_bug.cgi?id=655129
 *
 * Copyright (C) 2011 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License
 * as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Author: Colin Walters <walters@verbum.org>
 */

#include "config.h"

#define GNOME_DESKTOP_USE_UNSTABLE_API
#include "gnome-datetime-source.h"

#ifdef HAVE_TIMERFD
#include <sys/timerfd.h>
#include <unistd.h>
#include <string.h>
#endif

typedef struct _GDateTimeSource GDateTimeSource;
struct _GDateTimeSource
{
	GSource     source;
  
	gint64      real_expiration;
	gint64      wakeup_expiration;

	gboolean    cancel_on_set : 1;
	gboolean    initially_expired : 1;

	GPollFD     pollfd;
};

static inline void
g_datetime_source_reschedule (GDateTimeSource *datetime_source,
			      gint64                from_monotonic)
{
	datetime_source->wakeup_expiration = from_monotonic + G_TIME_SPAN_SECOND;
}

static gboolean
g_datetime_source_is_expired (GDateTimeSource *datetime_source)
{
	gint64 real_now;

	real_now = g_get_real_time ();

	if (datetime_source->initially_expired)
		return TRUE;
  
	if (datetime_source->real_expiration <= real_now)
		return TRUE;

	/* We can't really detect without system support when things change;
	 * so just trigger every second.
	 */
	if (datetime_source->cancel_on_set)
		return TRUE;

	return FALSE;
}

/* In prepare, we're just checking the monotonic time against
 * our projected wakeup.
 */
static gboolean
g_datetime_source_prepare (GSource *source,
			   gint    *timeout)
{
	GDateTimeSource *datetime_source = (GDateTimeSource*)source;
	gint64 monotonic_now;

#ifdef HAVE_TIMERFD
	if (datetime_source->pollfd.fd != -1) {
		*timeout = -1;
		return datetime_source->initially_expired;  /* Should be TRUE at most one time, FALSE forever after */
	}
#endif

	monotonic_now = g_source_get_time (source);

	if (monotonic_now < datetime_source->wakeup_expiration) {
		/* Round up to ensure that we don't try again too early */
		*timeout = (datetime_source->wakeup_expiration - monotonic_now + 999) / 1000;
		return FALSE;
	}

	*timeout = 0;
	return g_datetime_source_is_expired (datetime_source);
}

/* In check, we're looking at the wall clock.
 */
static gboolean 
g_datetime_source_check (GSource  *source)
{
	GDateTimeSource *datetime_source = (GDateTimeSource*)source;

#ifdef HAVE_TIMERFD
	if (datetime_source->pollfd.fd != -1)
		return datetime_source->pollfd.revents != 0;
#endif

	if (g_datetime_source_is_expired (datetime_source))
		return TRUE;

	g_datetime_source_reschedule (datetime_source, g_source_get_time (source));
  
	return FALSE;
}

static gboolean
g_datetime_source_dispatch (GSource    *source, 
			    GSourceFunc callback,
			    gpointer    user_data)
{
	GDateTimeSource *datetime_source = (GDateTimeSource*)source;

	datetime_source->initially_expired = FALSE;

	if (!callback) {
		g_warning ("Timeout source dispatched without callback\n"
			   "You must call g_source_set_callback().");
		return FALSE;
	}
  
	(callback) (user_data);

	/* Always false as this source is documented to run once */
	return FALSE;
}

static void
g_datetime_source_finalize (GSource *source)
{
#ifdef HAVE_TIMERFD
	GDateTimeSource *datetime_source = (GDateTimeSource*)source;
	if (datetime_source->pollfd.fd != -1)
		close (datetime_source->pollfd.fd);
#endif
}

static GSourceFuncs g_datetime_source_funcs = {
	g_datetime_source_prepare,
	g_datetime_source_check,
	g_datetime_source_dispatch,
	g_datetime_source_finalize
};

#ifdef HAVE_TIMERFD
static gboolean
g_datetime_source_init_timerfd (GDateTimeSource *datetime_source,
				gint64           expected_now_seconds,
				gint64           unix_seconds)
{
	struct itimerspec its;
	int settime_flags;

	datetime_source->pollfd.fd = timerfd_create (CLOCK_REALTIME, TFD_CLOEXEC);
	if (datetime_source->pollfd.fd == -1)
		return FALSE;

	memset (&its, 0, sizeof (its));
	its.it_value.tv_sec = (time_t) unix_seconds;

	/* http://article.gmane.org/gmane.linux.kernel/1132138 */
#ifndef TFD_TIMER_CANCEL_ON_SET
#define TFD_TIMER_CANCEL_ON_SET (1 << 1)
#endif

	settime_flags = TFD_TIMER_ABSTIME;
	if (datetime_source->cancel_on_set)
		settime_flags |= TFD_TIMER_CANCEL_ON_SET;

	if (timerfd_settime (datetime_source->pollfd.fd, settime_flags, &its, NULL) < 0) {
		close (datetime_source->pollfd.fd);
		datetime_source->pollfd.fd = -1;
		return FALSE;
	}

	/* Now we need to check that the clock didn't go backwards before we
	 * had the timerfd set up.  See
	 * https://bugzilla.gnome.org/show_bug.cgi?id=655129
	 */
	clock_gettime (CLOCK_REALTIME, &its.it_value);
	if (its.it_value.tv_sec < expected_now_seconds)
		datetime_source->initially_expired = TRUE;

	datetime_source->pollfd.events = G_IO_IN;

	g_source_add_poll ((GSource*) datetime_source, &datetime_source->pollfd);

	return TRUE;
}
#endif

/**
 * _gnome_date_time_source_new:
 * @now: The expected current time
 * @expiry: Time to await
 * @cancel_on_set: Also invoke callback if the system clock changes discontiguously
 *
 * This function is designed for programs that want to schedule an
 * event based on real (wall clock) time, as returned by
 * g_get_real_time().  For example, HOUR:MINUTE wall-clock displays
 * and calendaring software.  The callback will be invoked when the
 * specified wall clock time @expiry is reached.  This includes
 * events such as the system clock being set past the given time.
 *
 * Compare versus g_timeout_source_new() which is defined to use
 * monotonic time as returned by g_get_monotonic_time().
 *
 * The parameter @now is necessary to avoid a race condition in
 * between getting the current time and calling this function.
 *
 * If @cancel_on_set is given, the callback will also be invoked at
 * most a second after the system clock is changed.  This includes
 * being set backwards or forwards, and system
 * resume from suspend.  Not all operating systems allow detecting all
 * relevant events efficiently - this function may cause the process
 * to wake up once a second in those cases.
 *
 * A wall clock display should use @cancel_on_set; a calendaring
 * program shouldn't need to.
 *
 * Note that the return value from the associated callback will be
 * ignored; this is a one time watch.
 *
 * <note><para>This function currently does not detect time zone
 * changes.  On Linux, your program should also monitor the
 * <literal>/etc/timezone</literal> file using
 * #GFileMonitor.</para></note>
 *
 * <example id="gdatetime-example-watch"><title>Clock example</title><programlisting><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../glib/tests/glib-clock.c"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include></programlisting></example>
 *
 * Return value: A newly-constructed #GSource
 *
 * Since: 2.30
 **/
GSource *
_gnome_datetime_source_new (GDateTime  *now,
			    GDateTime  *expiry,
			    gboolean    cancel_on_set)
{
	GDateTimeSource *datetime_source;
	gint64 unix_seconds;

	unix_seconds = g_date_time_to_unix (expiry);

	datetime_source = (GDateTimeSource*) g_source_new (&g_datetime_source_funcs, sizeof (GDateTimeSource));

	datetime_source->cancel_on_set = cancel_on_set;

#ifdef HAVE_TIMERFD
	{
		gint64 expected_now_seconds = g_date_time_to_unix (now);
		if (g_datetime_source_init_timerfd (datetime_source, expected_now_seconds, unix_seconds))
			return (GSource*)datetime_source;
		/* Fall through to non-timerfd code */
	}
#endif

	datetime_source->real_expiration = unix_seconds * 1000000;
	g_datetime_source_reschedule (datetime_source, g_get_monotonic_time ());

	return (GSource*)datetime_source;
}

