/****************************** HEADER *******************************/

#ifndef __G_TIMEOUT_POOL_H__
#define __G_TIMEOUT_POOL_H__

#include <glib/gmain.h>

G_BEGIN_DECLS

typedef struct _GTimeoutPool GTimeoutPool;
typedef gboolean (* GTimeoutPoolFunc) (gint id, gpointer user_data);

GTimeoutPool *g_timeout_pool_new            (gint priority);

gint          g_timeout_pool_add_timeout    (GTimeoutPool *pool,
                                             guint interval,
                                             GTimeoutPoolFunc callback,
                                             gpointer user_data);

void          g_timeout_pool_remove_timeout (GTimeoutPool *pool, gint id);

void          g_timeout_pool_set_interval   (GTimeoutPool *pool,
                                             gint id, guint interval);

void          g_timeout_pool_set_callback   (GTimeoutPool *pool, gint id,
                                             GTimeoutPoolFunc callback);


G_END_DECLS

#endif //  __G_TIMEOUT_POOL_H__

/****************************** SOURCE *******************************/
#include <glib.h>
#include "gtimeoutpool.h"

typedef struct _GTimeout     GTimeout;

struct _GTimeout {
    gint             id;
    guint            interval;
    GTimeoutPoolFunc callback;
    gpointer         user_data;
    GTimeVal         expiration;
};

struct _GTimeoutPool {
    GSource  source;
    GList   *timeouts;
    gint     ready;
    gint     id;
};

static gboolean g_timeout_sort           (GTimeout *a, GTimeout *b);
static gboolean g_timeout_find_by_id     (GTimeout *timeout, gint id);
static gint     g_timeout_find_free_id   (GTimeoutPool *pool);

static void     g_timeout_set_expiration (GTimeout *timeout,
                                          GTimeVal *current_time);
static gboolean g_timeout_prepare        (GSource  *source,
                                          GTimeout *timeout,
                                          gint     *next_timeout);
static gboolean g_timeout_check          (GSource  *source,
                                          GTimeout *timeout);
static gboolean g_timeout_dispatch       (GSource  *source,
                                          GTimeout *timeout);

static gboolean g_timeout_pool_prepare   (GSource     *source,
                                          gint        *next_timeout);
static gboolean g_timeout_pool_check     (GSource     *source);
static gboolean g_timeout_pool_dispatch  (GSource     *source,
                                          GSourceFunc  callback,
                                          gpointer     user_data);
static void     g_timeout_pool_finalize  (GSource     *source);

static GSourceFuncs g_timeout_pool_funcs =
{
  g_timeout_pool_prepare,
  g_timeout_pool_check,
  g_timeout_pool_dispatch,
  g_timeout_pool_finalize
};

/**************************************************************
               List sorting, finding etc.
 **************************************************************/
static gboolean
g_timeout_sort(GTimeout *a, GTimeout *b)
{
  return ((a->expiration.tv_sec < b->expiration.tv_sec) ||
          ((a->expiration.tv_sec == b->expiration.tv_sec) &&
           (a->expiration.tv_usec <= b->expiration.tv_usec))) ? -1 : 1;
}

static gboolean
g_timeout_find_by_id (GTimeout *timeout, gint id) {
    return timeout->id - id;
}

static gint
g_timeout_find_free_id (GTimeoutPool *pool)
{
    GList    *l = pool->timeouts;
    GTimeout *timeout = NULL;
    gboolean  found = FALSE;
    gint      i;
    
    for (i = 0; i < G_MAXINT; i++) {
        while (l && l->data) {
            timeout = (GTimeout *)l->data;
            if (timeout->id == i)
                break;
            l = l->next;
        }
        if (timeout && timeout->id == i)
            continue;
        found = TRUE;
        break;
    }
    return found ? i : -1;
}

/**************************************************************
              Functions which deal with GTimeout
 **************************************************************/
static void
g_timeout_set_expiration (GTimeout *timeout,
                          GTimeVal *current_time)
{
  guint seconds = timeout->interval / 1000;
  guint msecs = timeout->interval - seconds * 1000;

  timeout->expiration.tv_sec = current_time->tv_sec + seconds;
  timeout->expiration.tv_usec = current_time->tv_usec + msecs * 1000;
  if (timeout->expiration.tv_usec >= 1000000)
    {
      timeout->expiration.tv_usec -= 1000000;
      timeout->expiration.tv_sec++;
    }
}

static gboolean
g_timeout_prepare (GSource  *source,
                   GTimeout *timeout,
                   gint     *next_timeout)
{
    glong sec;
    glong msec;
    GTimeVal current_time;
    
    g_source_get_current_time (source, &current_time);

    sec = timeout->expiration.tv_sec - current_time.tv_sec;
    msec = (timeout->expiration.tv_usec - current_time.tv_usec) / 1000;

    /* We do the following in a rather convoluted fashion to deal with
     * the fact that we don't have an integral type big enough to hold
     * the difference of two timevals in millseconds.
     */
    if (sec < 0 || (sec == 0 && msec < 0)) {
        msec = 0;
    } else {
        glong interval_sec = timeout->interval / 1000;
        glong interval_msec = timeout->interval % 1000;

        if (msec < 0) {
            msec += 1000;
            sec -= 1;
        }
      
        if (sec > interval_sec ||
            (sec == interval_sec && msec > interval_msec)) {
            /* The system time has been set backwards, so we
             * reset the expiration time to now + timeout_source->interval;
             * this at least avoids hanging for long periods of time.
             */
            g_timeout_set_expiration (timeout, &current_time);

            msec = MIN (G_MAXINT, timeout->interval);
        } else {
            msec = MIN (G_MAXINT, (guint)msec + 1000 * (guint)sec);
        }
    }

    *next_timeout = (gint)msec;
    return (msec == 0);
}

static gboolean 
g_timeout_check (GSource  *source,
                 GTimeout *timeout)
{
  GTimeVal current_time;
  g_source_get_current_time (source, &current_time);
  return ((timeout->expiration.tv_sec < current_time.tv_sec) ||
	  ((timeout->expiration.tv_sec == current_time.tv_sec) &&
	   (timeout->expiration.tv_usec <= current_time.tv_usec)));
}

static gboolean
g_timeout_dispatch (GSource    *source,
                    GTimeout   *timeout)
{
    if (timeout->callback == NULL) {
        g_warning ("Timeout dispatched without callback\n"
                   "You must call g_timeout_pool_set_callback().");
        return FALSE;
    }
    if (timeout->callback (timeout->id, timeout->user_data)) {
        GTimeVal current_time;

        g_source_get_current_time (source, &current_time);
        g_timeout_set_expiration (timeout, &current_time);

        return TRUE;
    } else return FALSE;
}

/**************************************************************
              Functions for GSource interaction
 **************************************************************/
static gboolean
g_timeout_pool_prepare (GSource  *source,
                        gint     *next_timeout)
{
    GTimeoutPool *pool    = (GTimeoutPool *)source;
    GList        *l       = g_list_first(pool->timeouts);
    GTimeout     *timeout;

    if (l) {
        timeout = (GTimeout *)l->data;
        return g_timeout_prepare(source, timeout, next_timeout);
    } else {
        /* infinite timeout */
        return -1;
    }
}

static gboolean
g_timeout_pool_check (GSource  *source)
{
    GTimeoutPool *pool    = (GTimeoutPool *)source;
    GList        *l       = g_list_first(pool->timeouts);
    GTimeout     *timeout;

    while (l && l->data) {
        timeout = (GTimeout *)l->data;
        if (g_timeout_check(source, timeout))
            pool->ready++;
        else
            break;
        l = l->next;
    }
    return (pool->ready > 0);
}

static gboolean
g_timeout_pool_dispatch (GSource    *source,
                         GSourceFunc callback,
                         gpointer    user_data)
{
    GTimeoutPool *pool     = (GTimeoutPool *)source;
    GList        *next, *l = g_list_first(pool->timeouts);
    GTimeout     *timeout;
    gboolean      sort_needed = FALSE;

    /* gmain.c may have predicted this and called dispatch, if so
     * we mush call check to refresh the ready count.
     */
    if (!pool->ready) {
        g_timeout_pool_check(source);
    }
    
    while (l && l->data && (pool->ready-- > 0)) {
        next    = l->next;
        timeout = (GTimeout *)l->data;
        if (g_timeout_dispatch(source, timeout)) {
            sort_needed = TRUE;
        } else {
            g_free(timeout);
            pool->timeouts =
                g_list_delete_link(pool->timeouts, l);
        }
        l = next;
    }
    if (sort_needed) {
        pool->timeouts =
            g_list_sort(pool->timeouts,
                        (GCompareFunc)g_timeout_sort);
    }
    pool->ready = 0;
    return TRUE;
}

static void 
g_timeout_pool_finalize (GSource *source)
{
    GTimeoutPool *pool = (GTimeoutPool *)source;
    GList        *l    = g_list_first(pool->timeouts);

    while (l && l->data) {
        g_free(l->data);
        l = l->next;
    }
    g_list_free(pool->timeouts);
}

/**************************************************************
                         Public API
 **************************************************************/
GTimeoutPool *
g_timeout_pool_new (gint priority)
{
    GTimeoutPool *pool;
    if ((pool =
         (GTimeoutPool *)g_source_new(&g_timeout_pool_funcs,
                                      sizeof(GTimeoutPool))) != NULL) {
        if (priority != G_PRIORITY_DEFAULT)
            g_source_set_priority ((GSource *)pool, priority);

        pool->id       = g_source_attach((GSource *)pool, NULL);
        g_source_unref ((GSource *)pool);
        return pool;
    }
    return NULL;
}

gint
g_timeout_pool_add_timeout (GTimeoutPool *pool,
                            guint interval,
                            GTimeoutPoolFunc callback,
                            gpointer user_data)
{
    GTimeout *timeout;
    gint ret = -1;
    if ((timeout = g_new(GTimeout, 1)) != NULL) {
        /* This should be better, how about (gint)timeout
         */
        if ((ret = timeout->id = g_timeout_find_free_id(pool)) < 0) {
            g_warning("Congradulations, you've busted my MAXINT limit of timeouts per pool");
            g_free(timeout);
        } else {
            GTimeVal current_time;
            g_source_get_current_time ((GSource *)pool, &current_time);

            timeout->interval  = interval;
            timeout->callback  = callback;
            timeout->user_data = user_data;

            g_timeout_set_expiration (timeout, &current_time);
            pool->timeouts =
                g_list_insert_sorted(pool->timeouts, timeout,
                                     (GCompareFunc)g_timeout_sort);
        }
    }
    return ret;
}

void
g_timeout_pool_remove_timeout (GTimeoutPool *pool,
                               gint id)
{
    GList    *l;
    if ((l =
         g_list_find_custom(pool->timeouts, (gpointer)id,
                            (GCompareFunc)g_timeout_find_by_id)) != NULL) {
        g_free(l->data);
        pool->timeouts = g_list_delete_link(pool->timeouts, l);
    }
}

void
g_timeout_pool_set_interval (GTimeoutPool *pool,
                             gint id, guint interval)
{
    GList    *l;
    GTimeout *timeout;
    GTimeVal  current_time;
    if ((l =
         g_list_find_custom(pool->timeouts, (gpointer)id,
                            (GCompareFunc)g_timeout_find_by_id)) != NULL) {
        timeout = (GTimeout *)l->data;
        timeout->interval = interval;

        g_source_get_current_time ((GSource *)pool, &current_time);
        g_timeout_set_expiration (timeout, &current_time);
        pool->timeouts =
            g_list_sort(pool->timeouts,
                        (GCompareFunc)g_timeout_sort);
    }
}

void
g_timeout_pool_set_callback (GTimeoutPool *pool,
                             gint id, GTimeoutPoolFunc callback)
{
    GList *l;
    if ((l =
         g_list_find_custom(pool->timeouts, (gpointer)id,
                            (GCompareFunc)g_timeout_find_by_id)) != NULL) {
        ((GTimeout *)l->data)->callback = callback;
    }
}
