/* eggfilemonitor.c: simple/pluggable file monitoring
 * Copyright (C) 2006 Emmanuele Bassi
 *
 * This library 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 library 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 library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 */

#include "config.h"

#include <glib.h>

#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

#ifdef G_OS_UNIX
#include <sys/wait.h>
#include <fcntl.h>
#endif

#include <glib/gi18n.h>
#include <glib/gstdio.h>

#include "eggfilemonitor.h"

typedef struct _EggFileMonitorSource EggFileMonitorSource;

struct _EggFileMonitorSource
{
  GSource source;
  
  gchar *uri;
  EggFileEvent last_event;
  gint last_poll_delta;
  time_t last_mtime;

  guint exists : 1;
};


static gboolean egg_file_monitor_prepare  (GSource     *source,
					   gint        *timeout);
static gboolean egg_file_monitor_check    (GSource     *source);
static gboolean egg_file_monitor_dispatch (GSource     *source,
					   GSourceFunc  callback,
					   gpointer     user_data);
static void     egg_file_monitor_finalize (GSource     *source);

static GSourceFuncs egg_file_monitor_funcs =
{
  egg_file_monitor_prepare,
  egg_file_monitor_check,
  egg_file_monitor_dispatch,
  egg_file_monitor_finalize
};

G_LOCK_DEFINE_STATIC (egg_file_monitor_hooks);
static EggFileMonitorHooks egg_file_monitor_hooks =
{
  NULL,
  NULL,
  NULL,
  NULL
};

static gpointer egg_file_monitor_hooks_data = NULL;

static gboolean
check_for_file_changes (EggFileMonitorSource *fm_source)
{
  GError *err;
  gchar *filename;
  struct stat stat_buf;
  int stat_res;

  err = NULL;
  filename = g_filename_from_uri (fm_source->uri, NULL, &err);
  if (err)
    {
      g_warning ("Unable to obtain the file name from URI '%s': %s",
                 fm_source->uri,
                 err->message);
      g_error_free (err);
      
      return FALSE;
    }
  
  stat_res = g_stat (filename, &stat_buf);
  g_free (filename);
  
  if (stat_res < 0)
    {
      if ((errno == ENOENT) && fm_source->exists)
        {
          fm_source->last_event = EGG_FILE_REMOVED_EVENT;
          fm_source->exists = FALSE;
          fm_source->last_mtime = (time_t) -1;

          return TRUE;
        }
      else
        return FALSE;
    }
  else
    {
      if (fm_source->last_mtime == stat_buf.st_mtime)
        return FALSE;
      
      if (!fm_source->exists)
        {
          fm_source->last_event = EGG_FILE_CREATED_EVENT;
          fm_source->last_mtime = stat_buf.st_mtime;
          fm_source->exists = TRUE;
        }
      else
        {
          fm_source->last_event = EGG_FILE_CHANGED_EVENT;
          fm_source->last_mtime = stat_buf.st_mtime;
        }
      
      return TRUE;
    }
  
  return FALSE;
}

#define POLL_DELTA_MAX		500
#define POLL_DELTA_QUANTUM	100

static gboolean
egg_file_monitor_prepare (GSource *source,
			  gint    *timeout)
{
  EggFileMonitorSource *fm_source = (EggFileMonitorSource *) source;
  gboolean was_changed;
  EggFileEvent old_last_event = fm_source->last_event;

  if (egg_file_monitor_hooks.check_uri)
    {
      EggFileEvent event;

      was_changed = (egg_file_monitor_hooks.check_uri) (fm_source->uri,
		      					&event,
							egg_file_monitor_hooks_data);
      fm_source->last_event = event;
    }
  else
    was_changed = check_for_file_changes (fm_source);
  
  if (was_changed)
    fm_source->last_poll_delta = 0;
  else
    {
      /* simple quantum-based timeout logic to avoid stat() storms; we
       * update the timeout depending on the last event: if the file
       * was removed, set the poll time to the maximum delta available;
       * otherwise, if it was last created or changed, add a quantum
       * of time until the timeout reaches the maximum delta available.
       */
      if (old_last_event == EGG_FILE_REMOVED_EVENT)
        fm_source->last_poll_delta = POLL_DELTA_MAX;
      else
        {
          gint new_delta;

	  new_delta = fm_source->last_poll_delta + POLL_DELTA_QUANTUM;
	  fm_source->last_poll_delta = MIN (new_delta, POLL_DELTA_MAX);
	}
      
    }

  *timeout = fm_source->last_poll_delta;

  return (fm_source->last_poll_delta == 0);
}

static gboolean
egg_file_monitor_check (GSource *source)
{
  EggFileMonitorSource *fm_source = (EggFileMonitorSource *) source;
 
  if (egg_file_monitor_hooks.check_uri)
    {
      EggFileEvent event;
      gboolean retval;
      
      retval = (egg_file_monitor_hooks.check_uri) (fm_source->uri,
		      				   &event,
						   egg_file_monitor_hooks_data);

      fm_source->last_event = event;

      return retval;
    }
  else
    return check_for_file_changes (fm_source);
}

static gboolean
egg_file_monitor_dispatch (GSource     *source,
			   GSourceFunc  callback,
			   gpointer     user_data)
{
  EggFileMonitorSource *fm_source;
  EggFileMonitorFunc fm_callback = (EggFileMonitorFunc) callback;
  
  fm_source = (EggFileMonitorSource *) source;
  
  if (!callback)
    {
      g_warning ("File monitor source dispatched without callback\n"
                 "You must call g_source_set_callback().");
      return FALSE;
    }

  if (fm_callback (fm_source->uri, fm_source->last_event, user_data))
    return TRUE;
  else
    {
      if (egg_file_monitor_hooks.cancel_monitor)
	(egg_file_monitor_hooks.cancel_monitor) (fm_source->uri,
						 egg_file_monitor_hooks_data);
      
      return FALSE;
    }
}

static void
egg_file_monitor_finalize (GSource *source)
{
  EggFileMonitorSource *fm_source;

  fm_source = (EggFileMonitorSource *) source;
  
  g_free (fm_source->uri);
}

/**
 * egg_file_monitor_source_new:
 * @uri: a valid URI
 * @events: the #EggFileEventMask mask to watch
 *
 * Creates a new file monitor source.
 *
 * The source will not initially be associated with any #GMainContext
 * and must be added to one with g_source_attach() before it will be
 * executed. Note that the default priority for file monitor sources
 * is %G_PRIORITY_DEFAULT_IDLE, as compared to other sources which
 * have a default priority of %G_PRIORITY_DEFAULT.
 *
 * Return value: the newly-created file monitor source
 *
 * Since: 2.12
 */
GSource *
egg_file_monitor_source_new (const gchar *uri)
{
  GSource *source;
  EggFileMonitorSource *fm_source;
  GError *err;
  gchar *filename;
  struct stat stat_buf;
  int stat_res;
  gboolean exists = FALSE;
  time_t last_mtime = (time_t) -1;
  
  g_return_val_if_fail (uri != NULL, NULL);

  err = NULL;
  filename = g_filename_from_uri (uri, NULL, &err);
  if (err)
    {
      g_warning ("Invalid URI '%s': %s", uri, err->message);
      g_error_free (err);

      return NULL;
    }

  stat_res = g_stat (filename, &stat_buf);
  g_free (filename);
  
  if (stat_res < 0)
    {
      if (errno == ENOENT)
        exists = FALSE;
      else
        {
          g_warning ("g_stat() of %s failed: %s", uri, g_strerror (errno));
	  
	  return NULL;
	}
    }
  else
    {
      exists = TRUE;
      last_mtime = stat_buf.st_mtime;
    }

  source = g_source_new (&egg_file_monitor_funcs, sizeof (EggFileMonitorSource));
  g_source_set_priority (source, G_PRIORITY_DEFAULT_IDLE);
  
  fm_source = (EggFileMonitorSource *) source;
  fm_source->uri = g_strdup (uri);
  fm_source->last_event = -1;
  fm_source->last_mtime = last_mtime;
  fm_source->exists = exists;
  
  return source;
}

/**
 * egg_file_monitor_add_full:
 * @priority: the priority of the file monitor source.  Typically, this
 *   whill be in the range between #G_PRIORITY_DEFAULT_IDLE and 
 *   #G_PRIORITY_HIGH_IDLE.
 * @uri: a valid URI
 * @function: function to be called
 * @data: data to pass to @function
 * @notify: function called when the file monitor is removed, or %NULL
 *
 * Sets a function to be called when the file pointed by @uri
 * is created, changed or removed, with the given priority.
 *
 * If the function returns %FALSE it is automatically removed
 * from the list of event sources and will not be called again.
 *
 * Note that the default implementation relies on a timed g_stat(),
 * and should therefore be used only on local files (using a file:/ URI).
 * If you want to control how the files are checked, you can override
 * the default implementation using egg_set_file_monitor_hooks().
 *
 * Return value: the ID (greater than 0) of the event source
 *
 * Since: 2.12
 */
guint
egg_file_monitor_add_full (gint                priority,
			   const gchar        *uri,
			   EggFileMonitorFunc  function,
			   gpointer            data,
			   GDestroyNotify      notify)
{
  GSource *source;
  guint id;
  
  g_return_val_if_fail (function != NULL, 0);
  
  source = egg_file_monitor_source_new (uri);
  
  if (priority != G_PRIORITY_DEFAULT_IDLE)
    g_source_set_priority (source, priority);
  
  g_source_set_callback (source, (GSourceFunc) function, data, notify);

  if (egg_file_monitor_hooks.add_monitor)
    {
      EggFileMonitorSource *fm_source = (EggFileMonitorSource *) source;

      (egg_file_monitor_hooks.add_monitor) (fm_source->uri,
					    egg_file_monitor_hooks_data);
    }
  
  id = g_source_attach (source, NULL);
  g_source_unref (source);
  
  return id;
}

/**
 * egg_file_monitor_add:
 * @uri: a valid URI
 * @function: function to be called
 * @data: data to be passed to @function
 *
 * Sets a function to be called when the file pointed by @uri
 * is created, changed or removed.
 *
 * The function is given the default idle priority, %G_PRIORITY_DEFAULT_IDLE.
 * If the function returns %FALSE it is automatically removed
 * from the list of event sources and will not be called again.
 *
 * Return value: the ID (greater than 0) of the event source
 *
 * Since: 2.12
 */
guint
egg_file_monitor_add (const gchar        *uri,
		      EggFileMonitorFunc  function,
		      gpointer            data)
{
  return egg_file_monitor_add_full (G_PRIORITY_DEFAULT_IDLE, uri, function, data, NULL);
}

/**
 * egg_set_file_monitor_hooks:
 * @hooks: a #EggFileMonitorHooks
 * @data: data to be passed to the hooks
 *
 * FIXME
 *
 * Since: 2.12
 */
void
egg_set_file_monitor_hooks (EggFileMonitorHooks *hooks,
			    gpointer             user_data)
{
  g_return_if_fail (hooks != NULL);

  G_LOCK (egg_file_monitor_hooks);
  
  if (hooks->add_monitor)
    egg_file_monitor_hooks.add_monitor = hooks->add_monitor;

  if (hooks->cancel_monitor)
    egg_file_monitor_hooks.cancel_monitor = hooks->cancel_monitor;

  if (hooks->check_uri)
    egg_file_monitor_hooks.check_uri = hooks->check_uri;

  if (hooks->error)
    egg_file_monitor_hooks.error = hooks->error;
  
  G_UNLOCK (egg_file_monitor_hooks);

  egg_file_monitor_hooks_data = user_data;
}
