/*
 * Copyright © 2008 Ryan Lortie
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of version 3 of the GNU General Public License as
 * published by the Free Software Foundation.   
 */

#include <sys/stat.h>

#include "dirwatch.h"

/* DirWatch
 *
 * a directory watcher utility for use by the trash:/ backend.
 *
 * A DirWatch monitors a given directory for existence under a very
 * specific set of circumstances.  When the directory comes into
 * existence, the create() callback is invoked.  When the directory
 * stops existing the destroy() callback is invoked.  If the directory
 * initially exists, then create() is invoked before the call to
 * dir_watch_new() returns.
 *
 * The directory to watch is considered to exist only if it is a
 * directory (and not a symlink) and its parent directory also exists.
 * A topdir must be given, which is always assumed to "exist".
 *
 * For example, if '/mnt/disk/.Trash/1000/files/' is monitored with
 * '/mnt/disk/' as a topdir then the following conditions must be true
 * in order for the directory to be reported as existing:
 *
 *   /mnt/disk/ is blindly assumed to exist
 *   /mnt/disk/.Trash must be a directory (not a symlink)
 *   /mnt/disk/.Trash/1000 must be a directory (not a symlink)
 *   /mnt/disk/.Trash/1000/files must be a directory (not a symlink)
 *
 * If any of these ceases to be true (even momentarily), the directory
 * will be reported as having been destroyed.  create() and destroy()
 * callbacks are never issued spuriously (ie: two calls to one
 * callback will never occur in a row).  Events where the directory
 * exists momentarily might be missed, but events where the directory
 * stops existing momentarily will (hopefully) always be reported.
 * The first call (if it happens) will always be to create().
 *
 * check() is only ever called in response to a call to
 * dir_watch_check() in which case it will be called only if the
 * watched directory was marked as having existed before the check and
 * is found to still exist.  This facilitates the checking that has to
 * occur in that case (ie: check the contents of the directory to make
 * sure that they are also unchanged).
 *
 * This implementation is currently tweaked a bit for how GFileMonitor
 * currently works with inotify.  If GFileMonitor's implementation is
 * changed it might be a good idea to take another look at this code.
 */

struct OPAQUE_TYPE__DirWatch
{
  GFile *directory;
  GFile *topdir;

  DirWatchFunc create;
  DirWatchFunc check;
  DirWatchFunc destroy;
  gpointer user_data;
  gboolean state;

  DirWatch *parent;

  GFileMonitor *parent_monitor;
};

#ifdef DIR_WATCH_DEBUG
# define dir_watch_created(watch) \
    G_STMT_START {                                              \
      char *path = g_file_get_path ((watch)->directory);        \
      g_print (">> created '%s'\n", path);                      \
      g_free (path);                                            \
      (watch)->create ((watch)->user_data);                     \
    } G_STMT_END

# define dir_watch_destroyed(watch) \
    G_STMT_START {                                              \
      char *path = g_file_get_path ((watch)->directory);        \
      g_print (">> destroyed '%s'\n", path);                    \
      g_free (path);                                            \
      (watch)->destroy ((watch)->user_data);                    \
    } G_STMT_END
#else
# define dir_watch_created(watch) (watch)->create ((watch)->user_data)
# define dir_watch_destroyed(watch) (watch)->destroy ((watch)->user_data)
#endif

#ifdef DIR_WATCH_DEBUG
#include <errno.h>
#endif

static gboolean
dir_exists (GFile *file)
{
  gboolean result;
  struct stat buf;
  char *path;

  path = g_file_get_path (file);
#ifdef DIR_WATCH_DEBUG
  errno = 0;
#endif
  result = !lstat (path, &buf) && S_ISDIR (buf.st_mode);

#ifdef DIR_WATCH_DEBUG
  g_print ("    lstat ('%s') -> is%s a directory (%s)\n",
           path, result ? "" : " not", g_strerror (errno));
#endif

  g_free (path);

  return result;
}

static void
dir_watch_parent_changed (GFileMonitor      *monitor,
                          GFile             *file,
                          GFile             *other_file,
                          GFileMonitorEvent  event_type,
                          gpointer           user_data)
{
  DirWatch *watch = user_data;

  g_assert (watch->parent_monitor == monitor);

  if (!g_file_equal (file, watch->directory))
    return;

  if (event_type == G_FILE_MONITOR_EVENT_CREATED)
    {
      if (watch->state)
        return;

      /* we were just created.  ensure that it's a directory. */
      if (dir_exists (file))
        {
          /* we're official now.  report it. */
          watch->state = TRUE;
          dir_watch_created (watch);
        }
    }
  else if (event_type == G_FILE_MONITOR_EVENT_DELETED)
    {
      if (!watch->state)
        return;

      watch->state = FALSE;
      dir_watch_destroyed (watch);
    }
}

static void
dir_watch_recursive_create (gpointer user_data)
{
  DirWatch *watch = user_data;
  GFile *parent;

  g_assert (watch->parent_monitor == NULL);

  parent = g_file_get_parent (watch->directory);
  watch->parent_monitor = g_file_monitor_directory (parent, 0,
                                                    NULL, NULL);
  g_object_unref (parent);
  g_signal_connect (watch->parent_monitor, "changed",
                    G_CALLBACK (dir_watch_parent_changed), watch);
  
  /* check if directory was created before we started to monitor */
  if (dir_exists (watch->directory))
    {
      watch->state = TRUE;
      dir_watch_created (watch);
    }
}

static void
dir_watch_recursive_check (gpointer user_data)
{
  DirWatch *watch = user_data;
  gboolean exists;
 
  exists = dir_exists (watch->directory);

  if (watch->state && exists)
    watch->check (watch->user_data);

  else if (!watch->state && exists)
    {
      watch->state = TRUE;
      dir_watch_created (watch);
    }
  else if (watch->state && !exists)
    {
      watch->state = FALSE;
      dir_watch_destroyed (watch);
    }
}

static void
dir_watch_recursive_destroy (gpointer user_data)
{
  DirWatch *watch = user_data;

  /* exactly one monitor should be active */
  g_assert (watch->parent_monitor != NULL);

  /* if we were monitoring the directory... */
  if (watch->state)
    {
      dir_watch_destroyed (watch);
      watch->state = FALSE;
    }

  g_object_unref (watch->parent_monitor);
  watch->parent_monitor = NULL;
}

DirWatch *
dir_watch_new (GFile        *directory,
               GFile        *topdir,
               DirWatchFunc  create,
               DirWatchFunc  check,
               DirWatchFunc  destroy,
               gpointer      user_data)
{
  DirWatch *watch;

  watch = g_slice_new0 (DirWatch);
  watch->create = create;
  watch->check = check;
  watch->destroy = destroy;
  watch->user_data = user_data;

  watch->directory = g_object_ref (directory);
  watch->topdir = g_object_ref (topdir);

  /* the top directory always exists */
  if (g_file_equal (directory, topdir))
    {
      dir_watch_created (watch);
      watch->state = TRUE;
    }

  else
    {
      GFile *parent;

      parent = g_file_get_parent (directory);
      g_assert (parent != NULL);

      watch->parent = dir_watch_new (parent, topdir,
                                     dir_watch_recursive_create,
                                     dir_watch_recursive_check,
                                     dir_watch_recursive_destroy,
                                     watch);

      g_object_unref (parent);
    }

  return watch;
}

void
dir_watch_free (DirWatch *watch)
{
  if (watch != NULL)
    {
      if (watch->parent_monitor)
        g_object_unref (watch->parent_monitor);

      g_object_unref (watch->directory);
      g_object_unref (watch->topdir);

      dir_watch_free (watch->parent);

      g_slice_free (DirWatch, watch);
    }
}

/**
 * dir_watch_check:
 * @watch: a #DirWatch
 *
 * Emit missed events.
 *
 * This function is called on a DirWatch that might have missed events
 * (because it is watching on an NFS mount, for example).
 * 
 * This function will manually check if any directories have come into
 * or gone out of existence and will emit created or destroyed callbacks
 * as appropriate.
 *
 * Additionally, if a directory is found to still exist, the checked
 * callback will be emitted.
 **/
void
dir_watch_check (DirWatch *watch)
{
  if (watch->parent == NULL)
    {
      g_assert (watch->state);

      watch->check (watch->user_data);
      return;
    }

  dir_watch_check (watch->parent);
}
