/* dzl-recursive-file-monitor.c
 *
 * Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
 *
 * 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, either version 3 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "dzl-recursive-file-monitor"

#include "config.h"

#include <limits.h>
#include <stdlib.h>

#include "files/dzl-recursive-file-monitor.h"
#include "util/dzl-macros.h"

#define MONITOR_FLAGS 0

/**
 * SECTION:dzl-recursive-file-monitor
 * @title: DzlRecursiveFileMonitor
 * @short_description: a recursive directory monitor
 *
 * This works by creating a #GFileMonitor for each directory underneath a root
 * directory (and recursively beyond that).
 *
 * This is only designed for use on Linux, where we are using a single inotify
 * FD. You can still hit the max watch limit, but it is much higher than the FD
 * limit.
 *
 * Since: 3.28
 */

struct _DzlRecursiveFileMonitor
{
  GObject                 parent_instance;

  GFile                  *root;
  GCancellable           *cancellable;

  GHashTable             *monitors_by_file;
  GHashTable             *files_by_monitor;

  DzlRecursiveIgnoreFunc  ignore_func;
  gpointer                ignore_func_data;
  GDestroyNotify          ignore_func_data_destroy;
};

enum {
  PROP_0,
  PROP_ROOT,
  N_PROPS
};

enum {
  CHANGED,
  N_SIGNALS
};

G_DEFINE_TYPE (DzlRecursiveFileMonitor, dzl_recursive_file_monitor, G_TYPE_OBJECT)

static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];

static void
dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self,
                                  GFile                   *dir,
                                  GFileMonitor            *monitor);

static void
dzl_recursive_file_monitor_unwatch (DzlRecursiveFileMonitor *self,
                                    GFile                   *file)
{
  GFileMonitor *monitor;

  dzl_assert_is_main_thread ();
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_FILE (file));

  monitor = g_hash_table_lookup (self->monitors_by_file, file);

  if (monitor != NULL)
    {
      g_object_ref (monitor);
      g_file_monitor_cancel (monitor);
      g_hash_table_remove (self->monitors_by_file, file);
      g_hash_table_remove (self->files_by_monitor, monitor);
      g_object_unref (monitor);
    }
}

static void
dzl_recursive_file_monitor_collect_recursive (GPtrArray    *dirs,
                                              GFile        *parent,
                                              GCancellable *cancellable)
{
  g_autoptr(GFileEnumerator) enumerator = NULL;
  g_autoptr(GError) error = NULL;

  g_assert (dirs != NULL);
  g_assert (G_IS_FILE (parent));
  g_assert (G_IS_CANCELLABLE (cancellable));

  enumerator = g_file_enumerate_children (parent,
                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                          cancellable, &error);

  if (error != NULL)
    {
      g_warning ("Failed to iterate children: %s", error->message);
      g_clear_error (&error);
    }

  if (enumerator != NULL)
    {
      gpointer infoptr;

      while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
        {
          g_autoptr(GFileInfo) info = infoptr;

          if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
            {
              const gchar *name = g_file_info_get_name (info);
              g_autoptr(GFile) child = g_file_get_child (parent, name);

              /*
               * We add the child, and then recurse into the child immediately
               * so that we can keep the invariant that all descendants
               * immediately follow their ancestor. This allows us to simplify
               * our ignored-directory checks when we get back to the main
               * thread.
               */

              g_ptr_array_add (dirs, g_object_ref (child));
              dzl_recursive_file_monitor_collect_recursive (dirs, child, cancellable);
            }
        }

      g_file_enumerator_close (enumerator, cancellable, NULL);
      g_clear_object (&enumerator);
    }
}

static GFile *
resolve_file (GFile *file)
{
  g_autofree gchar *orig_path = NULL;
  g_autoptr(GFile) new_file = NULL;
  char *real_path;

  g_assert (G_IS_FILE (file));

  /*
   * The goal here is to work our way up to the root and resolve any
   * symlinks in the path. If the file is not native, we don't care
   * about symlinks.
   */
  if (!g_file_is_native (file))
    return g_object_ref (file);

  orig_path = g_file_get_path (file);
  real_path = realpath (orig_path, NULL);

  /* unlikely, but PATH_MAX exceeded */
  if (real_path == NULL)
    return g_object_ref (file);

  new_file = g_file_new_for_path (real_path);
  free (real_path);

  return g_steal_pointer (&new_file);
}

static void
dzl_recursive_file_monitor_collect_worker (GTask        *task,
                                           gpointer      source_object,
                                           gpointer      task_data,
                                           GCancellable *cancellable)
{
  g_autoptr(GPtrArray) dirs = NULL;
  g_autoptr(GFile) resolved = NULL;
  GFile *root = task_data;

  g_assert (G_IS_TASK (task));
  g_assert (G_IS_FILE (root));

  /* The first thing we want to do is resolve any symlinks out of
   * the path so that we are consistently working with the real
   * system path. This improves interaction with other APIs that
   * might not have given the callee back the symlink'd path and
   * instead the real path.
   */
  resolved = resolve_file (root);

  dirs = g_ptr_array_new_with_free_func (g_object_unref);
  g_ptr_array_add (dirs, g_object_ref (resolved));
  dzl_recursive_file_monitor_collect_recursive (dirs, resolved, cancellable);

  g_task_return_pointer (task,
                         g_steal_pointer (&dirs),
                         (GDestroyNotify)g_ptr_array_unref);
}

static void
dzl_recursive_file_monitor_collect (DzlRecursiveFileMonitor *self,
                                    GFile                   *root,
                                    GCancellable            *cancellable,
                                    GAsyncReadyCallback      callback,
                                    gpointer                 user_data)
{
  g_autoptr(GTask) task = NULL;

  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_FILE (root));
  g_assert (G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, dzl_recursive_file_monitor_collect);
  g_task_set_priority (task, G_PRIORITY_LOW);
  g_task_set_task_data (task, g_object_ref (root), g_object_unref);
  g_task_run_in_thread (task, dzl_recursive_file_monitor_collect_worker);
}

static GPtrArray *
dzl_recursive_file_monitor_collect_finish (DzlRecursiveFileMonitor  *self,
                                           GAsyncResult             *result,
                                           GError                  **error)
{
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_TASK (result));
  g_assert (g_task_is_valid (G_TASK (result), self));

  return g_task_propagate_pointer (G_TASK (result), error);
}

static gboolean
dzl_recursive_file_monitor_ignored (DzlRecursiveFileMonitor *self,
                                    GFile                   *file)
{
  dzl_assert_is_main_thread ();
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_FILE (file));

  if (self->ignore_func != NULL)
    return self->ignore_func (file, self->ignore_func_data);

  return FALSE;
}

static void
dzl_recursive_file_monitor_changed (DzlRecursiveFileMonitor *self,
                                    GFile                   *file,
                                    GFile                   *other_file,
                                    GFileMonitorEvent        event,
                                    GFileMonitor            *monitor)
{
  dzl_assert_is_main_thread ();
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_FILE (file));
  g_assert (!other_file || G_IS_FILE (file));
  g_assert (G_IS_FILE_MONITOR (monitor));

  if (g_cancellable_is_cancelled (self->cancellable))
    return;

  if (dzl_recursive_file_monitor_ignored (self, file))
    return;

  if (event == G_FILE_MONITOR_EVENT_DELETED)
    {
      if (g_hash_table_contains (self->monitors_by_file, file))
        dzl_recursive_file_monitor_unwatch (self, file);
    }
  else if (event == G_FILE_MONITOR_EVENT_CREATED)
    {
      if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY)
        {
          g_autoptr(GPtrArray) dirs = NULL;

          dirs = g_ptr_array_new_with_free_func (g_object_unref);
          g_ptr_array_add (dirs, g_object_ref (file));

          dzl_recursive_file_monitor_collect_recursive (dirs, file, self->cancellable);

          for (guint i = 0; i < dirs->len; i++)
            {
              g_autoptr(GFileMonitor) dir_monitor = NULL;
              GFile *dir = g_ptr_array_index (dirs, i);

              if (!!(dir_monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, NULL)))
                dzl_recursive_file_monitor_track (self, dir, dir_monitor);
            }
        }
    }

  g_signal_emit (self, signals [CHANGED], 0, file, other_file, event);
}


static void
dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self,
                                  GFile                   *dir,
                                  GFileMonitor            *monitor)
{
  dzl_assert_is_main_thread ();
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_FILE (dir));
  g_assert (G_IS_FILE_MONITOR (monitor));

  g_hash_table_insert (self->monitors_by_file,
                       g_object_ref (dir),
                       g_object_ref (monitor));

  g_hash_table_insert (self->files_by_monitor,
                       g_object_ref (monitor),
                       g_object_ref (dir));

  g_signal_connect_object (monitor,
                           "changed",
                           G_CALLBACK (dzl_recursive_file_monitor_changed),
                           self,
                           G_CONNECT_SWAPPED);
}

static void
dzl_recursive_file_monitor_start_cb (GObject      *object,
                                     GAsyncResult *result,
                                     gpointer      user_data)
{
  DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object;
  g_autoptr(GPtrArray) dirs = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GTask) task = user_data;

  dzl_assert_is_main_thread ();
  g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (G_IS_TASK (task));

  dirs = dzl_recursive_file_monitor_collect_finish (self, result, &error);

  if (dirs == NULL)
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  for (guint i = 0; i < dirs->len; i++)
    {
      GFile *dir = g_ptr_array_index (dirs, i);
      g_autoptr(GFileMonitor) monitor = NULL;

      g_assert (G_IS_FILE (dir));

      if (dzl_recursive_file_monitor_ignored (self, dir))
        {
          /*
           * Skip ahead to the next directory that does not have this directory
           * as a prefix. We can do this because we know the descendants are
           * guaranteed to immediately follow this directory.
           */

          for (guint j = i + 1; j < dirs->len; j++, i++)
            {
              GFile *next = g_ptr_array_index (dirs, j);

              if (!g_file_has_prefix (next, dir))
                break;
            }

          continue;
        }

      monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, &error);

      if (monitor == NULL)
        {
          g_warning ("Failed to monitor directory: %s", error->message);
          g_clear_error (&error);
          continue;
        }

      dzl_recursive_file_monitor_track (self, dir, monitor);
    }

  g_task_return_boolean (task, TRUE);
}

void
dzl_recursive_file_monitor_start_async (DzlRecursiveFileMonitor *self,
                                        GCancellable            *cancellable,
                                        GAsyncReadyCallback      callback,
                                        gpointer                 user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, dzl_recursive_file_monitor_start_async);
  g_task_set_return_on_cancel (task, TRUE);
  g_task_set_task_data (task, g_object_ref (self->root), g_object_unref);
  g_task_set_priority (task, G_PRIORITY_LOW);

  if (self->root == NULL)
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               G_IO_ERROR_INVAL,
                               "Cannot start file monitor, no root directory set");
      return;
    }

  dzl_recursive_file_monitor_collect (self,
                                      self->root,
                                      self->cancellable,
                                      dzl_recursive_file_monitor_start_cb,
                                      g_steal_pointer (&task));
}

gboolean
dzl_recursive_file_monitor_start_finish (DzlRecursiveFileMonitor  *self,
                                         GAsyncResult             *result,
                                         GError                  **error)
{
  g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);
  g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

static void
dzl_recursive_file_monitor_constructed (GObject *object)
{
  DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object;

  G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->constructed (object);

  if (self->root == NULL)
    g_warning ("%s created without a root directory", G_OBJECT_TYPE_NAME (self));
}

static void
dzl_recursive_file_monitor_dispose (GObject *object)
{
  DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object;

  g_cancellable_cancel (self->cancellable);
  dzl_recursive_file_monitor_set_ignore_func (self, NULL, NULL, NULL);

  g_hash_table_remove_all (self->files_by_monitor);
  g_hash_table_remove_all (self->monitors_by_file);

  G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->dispose (object);
}

static void
dzl_recursive_file_monitor_finalize (GObject *object)
{
  DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object;

  g_clear_object (&self->root);
  g_clear_object (&self->cancellable);

  g_clear_pointer (&self->files_by_monitor, g_hash_table_unref);
  g_clear_pointer (&self->monitors_by_file, g_hash_table_unref);

  G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->finalize (object);
}

static void
dzl_recursive_file_monitor_get_property (GObject    *object,
                                         guint       prop_id,
                                         GValue     *value,
                                         GParamSpec *pspec)
{
  DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object);

  switch (prop_id)
    {
    case PROP_ROOT:
      g_value_set_object (value, dzl_recursive_file_monitor_get_root (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
dzl_recursive_file_monitor_set_property (GObject      *object,
                                         guint         prop_id,
                                         const GValue *value,
                                         GParamSpec   *pspec)
{
  DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object);

  switch (prop_id)
    {
    case PROP_ROOT:
      self->root = g_value_dup_object (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
dzl_recursive_file_monitor_class_init (DzlRecursiveFileMonitorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = dzl_recursive_file_monitor_constructed;
  object_class->dispose = dzl_recursive_file_monitor_dispose;
  object_class->finalize = dzl_recursive_file_monitor_finalize;
  object_class->get_property = dzl_recursive_file_monitor_get_property;
  object_class->set_property = dzl_recursive_file_monitor_set_property;

  properties [PROP_ROOT] =
    g_param_spec_object ("root",
                         "Root",
                         "The root directory to monitor",
                         G_TYPE_FILE,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  /**
   * DzlRecursiveFileMonitor::changed:
   * @self: a #DzlRecursiveFileMonitor
   * @file: a #GFile
   * @other_file: (nullable): a #GFile for the other file when applicable
   * @event: the #GFileMonitorEvent event
   *
   * This event is similar to #GFileMonitor::changed but can be fired from
   * any of the monitored directories in the recursive mount.
   *
   * Since: 3.28
   */
  signals [CHANGED] =
    g_signal_new ("changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL, NULL,
                  G_TYPE_NONE, 3, G_TYPE_FILE, G_TYPE_FILE, G_TYPE_FILE_MONITOR_EVENT);
}

static void
dzl_recursive_file_monitor_init (DzlRecursiveFileMonitor *self)
{
  self->cancellable = g_cancellable_new ();
  self->files_by_monitor = g_hash_table_new_full (NULL, NULL, g_object_unref, g_object_unref);
  self->monitors_by_file = g_hash_table_new_full (g_file_hash,
                                                  (GEqualFunc) g_file_equal,
                                                  g_object_unref,
                                                  g_object_unref);
}

DzlRecursiveFileMonitor *
dzl_recursive_file_monitor_new (GFile *file)
{
  g_return_val_if_fail (G_IS_FILE (file), NULL);

  return g_object_new (DZL_TYPE_RECURSIVE_FILE_MONITOR,
                       "root", file,
                       NULL);
}

/**
 * dzl_recursive_file_monitor_cancel:
 * @self: a #DzlRecursiveFileMonitor
 *
 * Cancels the recursive file monitor.
 *
 * Since: 3.28
 */
void
dzl_recursive_file_monitor_cancel (DzlRecursiveFileMonitor *self)
{
  g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self));

  g_object_run_dispose (G_OBJECT (self));
}

/**
 * dzl_recursive_file_monitor_get_root:
 * @self: a #DzlRecursiveFileMonitor
 *
 * Gets the root directory used forthe file monitor.
 *
 * Returns: (transfer none): a #GFile
 *
 * Since: 3.28
 */
GFile *
dzl_recursive_file_monitor_get_root (DzlRecursiveFileMonitor *self)
{
  g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), NULL);

  return self->root;
}

/**
 * dzl_recursive_file_monitor_set_ignore_func:
 * @self: a #DzlRecursiveFileMonitor
 * @ignore_func: (scope async): a #DzlRecursiveIgnoreFunc
 * @ignore_func_data: closure data for @ignore_func
 * @ignore_func_data_destroy: destroy notify for @ignore_func_data
 *
 * Sets a callback function to determine if a #GFile should be ignored
 * from signal emission.
 *
 * @ignore_func will always be called from the applications main thread.
 *
 * If @ignore_func is %NULL, it is set to the default which does not
 * ignore any files or directories.
 *
 * Since: 3.28
 */
void
dzl_recursive_file_monitor_set_ignore_func (DzlRecursiveFileMonitor *self,
                                            DzlRecursiveIgnoreFunc   ignore_func,
                                            gpointer                 ignore_func_data,
                                            GDestroyNotify           ignore_func_data_destroy)
{
  dzl_assert_is_main_thread ();
  g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self));

  if (ignore_func == NULL)
    {
      ignore_func_data = NULL;
      ignore_func_data_destroy = NULL;
    }

  if (self->ignore_func_data && self->ignore_func_data_destroy)
    {
      gpointer data = self->ignore_func_data;
      GDestroyNotify notify = self->ignore_func_data_destroy;

      self->ignore_func = NULL;
      self->ignore_func_data = NULL;
      self->ignore_func_data_destroy = NULL;

      notify (data);
    }

  self->ignore_func = ignore_func;
  self->ignore_func_data = ignore_func_data;
  self->ignore_func_data_destroy = ignore_func_data_destroy;
}
