/* dzl-directory-model.c
 *
 * Copyright (C) 2015 Christian Hergert <christian hergert me>
 *
 * This file is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * This file 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
 * Lesser 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-directory-model"

#include "config.h"

#include <glib/gi18n.h>

#include "dzl-directory-model.h"

#define NEXT_FILES_CHUNK_SIZE 25

struct _DzlDirectoryModel
{
  GObject                       parent_instance;

  GCancellable                 *cancellable;
  GFile                        *directory;
  GSequence                    *items;
  GFileMonitor                 *monitor;

  DzlDirectoryModelVisibleFunc  visible_func;
  gpointer                      visible_func_data;
  GDestroyNotify                visible_func_destroy;
};

static void list_model_iface_init      (GListModelInterface *iface);
static void dzl_directory_model_reload (DzlDirectoryModel   *self);

G_DEFINE_TYPE_EXTENDED (DzlDirectoryModel, dzl_directory_model, G_TYPE_OBJECT, 0,
                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))

enum {
  PROP_0,
  PROP_DIRECTORY,
  LAST_PROP
};

static GParamSpec *gParamSpecs [LAST_PROP];

static gint
compare_display_name (gconstpointer a,
                      gconstpointer b,
                      gpointer      data)
{
  GFileInfo *file_info_a = (GFileInfo *)a;
  GFileInfo *file_info_b = (GFileInfo *)b;
  const gchar *display_name_a = g_file_info_get_display_name (file_info_a);
  const gchar *display_name_b = g_file_info_get_display_name (file_info_b);
  g_autofree gchar *name_a = g_utf8_collate_key_for_filename (display_name_a, -1);
  g_autofree gchar *name_b = g_utf8_collate_key_for_filename (display_name_b, -1);

  return g_utf8_collate (name_a, name_b);
}

static gint
compare_directories_first (gconstpointer a,
                           gconstpointer b,
                           gpointer      data)
{
  GFileInfo *file_info_a = (GFileInfo *)a;
  GFileInfo *file_info_b = (GFileInfo *)b;
  GFileType file_type_a = g_file_info_get_file_type (file_info_a);
  GFileType file_type_b = g_file_info_get_file_type (file_info_b);

  if (file_type_a == file_type_b)
    return compare_display_name (a, b, data);

  return (file_type_a == G_FILE_TYPE_DIRECTORY) ? -1 : 1;
}

static void
dzl_directory_model_remove_all (DzlDirectoryModel *self)
{
  GSequence *seq;
  guint length;

  g_assert (DZL_IS_DIRECTORY_MODEL (self));

  length = g_sequence_get_length (self->items);

  if (length > 0)
    {
      seq = self->items;
      self->items = g_sequence_new (g_object_unref);
      g_list_model_items_changed (G_LIST_MODEL (self), 0, length, 0);
      g_sequence_free (seq);
    }
}

static void
dzl_directory_model_take_item (DzlDirectoryModel *self,
                               GFileInfo         *file_info)
{
  GSequenceIter *iter;
  guint position;

  g_assert (DZL_IS_DIRECTORY_MODEL (self));
  g_assert (G_IS_FILE_INFO (file_info));

  if ((self->visible_func != NULL) &&
      !self->visible_func (self, self->directory, file_info, self->visible_func_data))
    {
      g_object_unref (file_info);
      return;
    }

  iter = g_sequence_insert_sorted (self->items,
                                   file_info,
                                   compare_directories_first,
                                   NULL);
  position = g_sequence_iter_get_position (iter);
  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
}

static void
dzl_directory_model_next_files_cb (GObject      *object,
                                   GAsyncResult *result,
                                   gpointer      user_data)
{
  GFileEnumerator *enumerator = (GFileEnumerator *)object;
  g_autoptr(GTask) task = user_data;
  DzlDirectoryModel *self;
  GList *files;
  GList *iter;

  g_assert (G_IS_FILE_ENUMERATOR (enumerator));
  g_assert (G_IS_TASK (task));

  if (!(files = g_file_enumerator_next_files_finish (enumerator, result, NULL)))
    return;

  self = g_task_get_source_object (task);

  g_assert (DZL_IS_DIRECTORY_MODEL (self));

  for (iter = files; iter; iter = iter->next)
    {
      GFileInfo *file_info = iter->data;

      dzl_directory_model_take_item (self, file_info);
    }

  g_list_free (files);

  g_file_enumerator_next_files_async (enumerator,
                                      NEXT_FILES_CHUNK_SIZE,
                                      G_PRIORITY_LOW,
                                      g_task_get_cancellable (task),
                                      dzl_directory_model_next_files_cb,
                                      g_object_ref (task));
}

static void
dzl_directory_model_enumerate_children_cb (GObject      *object,
                                           GAsyncResult *result,
                                           gpointer      user_data)
{
  GFile *directory = (GFile *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GFileEnumerator) enumerator = NULL;

  g_assert (G_IS_FILE (directory));
  g_assert (G_IS_TASK (task));

  if (!(enumerator = g_file_enumerate_children_finish (directory, result, NULL)))
    return;

  g_file_enumerator_next_files_async (enumerator,
                                      NEXT_FILES_CHUNK_SIZE,
                                      G_PRIORITY_LOW,
                                      g_task_get_cancellable (task),
                                      dzl_directory_model_next_files_cb,
                                      g_object_ref (task));
}

static void
dzl_directory_model_remove_file (DzlDirectoryModel *self,
                                 GFile             *file)
{
  g_autofree gchar *name = NULL;
  GSequenceIter *iter;

  g_assert (G_IS_FILE (file));

  name = g_file_get_basename (file);

  /*
   * We have to lookup linearly since the items will likely be
   * sorted by name, directory, file-system ordering, or some
   * combination thereof.
   */

  for (iter = g_sequence_get_begin_iter (self->items);
       !g_sequence_iter_is_end (iter);
       iter = g_sequence_iter_next (iter))
    {
      GFileInfo *file_info = g_sequence_get (iter);
      const gchar *file_info_name = g_file_info_get_name (file_info);

      if (0 == g_strcmp0 (file_info_name, name))
        {
          guint position;

          position = g_sequence_iter_get_position (iter);
          g_sequence_remove (iter);
          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
          break;
        }
    }
}

static void
dzl_directory_model_directory_changed (DzlDirectoryModel *self,
                                       GFile             *file,
                                       GFile             *other_file,
                                       GFileMonitorEvent  event_type,
                                       GFileMonitor      *monitor)
{
  g_assert (DZL_IS_DIRECTORY_MODEL (self));

  switch ((int)event_type)
    {
    case G_FILE_MONITOR_EVENT_CREATED:
      /*
       * TODO: incremental changes
       *
       * When adding, we need to first add the GFileInfo for the file with all
       * of the attributes we load in the primary case.
       */
      dzl_directory_model_reload (self);
      break;

    case G_FILE_MONITOR_EVENT_DELETED:
      dzl_directory_model_remove_file (self, file);
      break;

    default:
      break;
    }
}

static void
dzl_directory_model_reload (DzlDirectoryModel *self)
{
  g_assert (DZL_IS_DIRECTORY_MODEL (self));

  if (self->monitor != NULL)
    {
      g_file_monitor_cancel (self->monitor);
      g_signal_handlers_disconnect_by_func (self->monitor,
                                            G_CALLBACK (dzl_directory_model_directory_changed),
                                            self);
      g_clear_object (&self->monitor);
    }

  if (self->cancellable != NULL)
    {
      g_cancellable_cancel (self->cancellable);
      g_clear_object (&self->cancellable);
    }

  dzl_directory_model_remove_all (self);

  if (self->directory != NULL)
    {
      g_autoptr(GTask) task = NULL;

      self->cancellable = g_cancellable_new ();
      task = g_task_new (self, self->cancellable, NULL, NULL);

      g_file_enumerate_children_async (self->directory,
                                       G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
                                       G_FILE_ATTRIBUTE_STANDARD_NAME","
                                       G_FILE_ATTRIBUTE_STANDARD_TYPE","
                                       G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
                                       G_FILE_QUERY_INFO_NONE,
                                       G_PRIORITY_LOW,
                                       self->cancellable,
                                       dzl_directory_model_enumerate_children_cb,
                                       g_object_ref (task));

      self->monitor = g_file_monitor_directory (self->directory,
                                                G_FILE_MONITOR_NONE,
                                                self->cancellable,
                                                NULL);

      g_signal_connect_object (self->monitor,
                               "changed",
                               G_CALLBACK (dzl_directory_model_directory_changed),
                               self,
                               G_CONNECT_SWAPPED);
    }
}

static void
dzl_directory_model_finalize (GObject *object)
{
  DzlDirectoryModel *self = (DzlDirectoryModel *)object;

  g_clear_object (&self->cancellable);
  g_clear_object (&self->directory);
  g_clear_pointer (&self->items, g_sequence_free);

  if (self->visible_func_destroy)
    self->visible_func_destroy (self->visible_func_data);

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

static void
dzl_directory_model_get_property (GObject    *object,
                                  guint       prop_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  DzlDirectoryModel *self = DZL_DIRECTORY_MODEL (object);

  switch (prop_id)
    {
    case PROP_DIRECTORY:
      g_value_set_object (value, dzl_directory_model_get_directory (self));
      break;

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

static void
dzl_directory_model_set_property (GObject      *object,
                                  guint         prop_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  DzlDirectoryModel *self = DZL_DIRECTORY_MODEL (object);

  switch (prop_id)
    {
    case PROP_DIRECTORY:
      dzl_directory_model_set_directory (self, g_value_get_object (value));
      break;

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

static void
dzl_directory_model_class_init (DzlDirectoryModelClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = dzl_directory_model_finalize;
  object_class->get_property = dzl_directory_model_get_property;
  object_class->set_property = dzl_directory_model_set_property;

  gParamSpecs [PROP_DIRECTORY] =
    g_param_spec_object ("directory",
                         _("Directory"),
                         _("The directory to list files from."),
                         G_TYPE_FILE,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
}

static void
dzl_directory_model_init (DzlDirectoryModel *self)
{
  self->items = g_sequence_new (g_object_unref);
}

/**
 * dzl_directory_model_new:
 * @directory: A #GFile
 *
 * Creates a new #DzlDirectoryModel using @directory as the directory to monitor.
 *
 * Returns: (transfer full): A newly created #DzlDirectoryModel
 */
GListModel *
dzl_directory_model_new (GFile *directory)
{
  g_return_val_if_fail (G_IS_FILE (directory), NULL);

  return g_object_new (DZL_TYPE_DIRECTORY_MODEL,
                       "directory", directory,
                       NULL);
}

/**
 * dzl_directory_model_get_directory:
 * @self: a #DzlDirectoryModel
 *
 * Gets the directory the model is observing.
 *
 * Returns: (transfer none): A #GFile
 */
GFile *
dzl_directory_model_get_directory (DzlDirectoryModel *self)
{
  g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), NULL);

  return self->directory;
}

void
dzl_directory_model_set_directory (DzlDirectoryModel *self,
                                   GFile             *directory)
{
  g_return_if_fail (DZL_IS_DIRECTORY_MODEL (self));
  g_return_if_fail (!directory || G_IS_FILE (directory));

  if (g_set_object (&self->directory, directory))
    {
      dzl_directory_model_reload (self);
      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_DIRECTORY]);
    }
}

static guint
dzl_directory_model_get_n_items (GListModel *model)
{
  DzlDirectoryModel *self = (DzlDirectoryModel *)model;

  g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), 0);

  return g_sequence_get_length (self->items);
}

static GType
dzl_directory_model_get_item_type (GListModel *model)
{
  return G_TYPE_FILE_INFO;
}

static gpointer
dzl_directory_model_get_item (GListModel *model,
                              guint       position)
{
  DzlDirectoryModel *self = (DzlDirectoryModel *)model;
  GSequenceIter *iter;
  gpointer ret;

  g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), NULL);

  if ((iter = g_sequence_get_iter_at_pos (self->items, position)) &&
      (ret = g_sequence_get (iter)))
    return g_object_ref (ret);

  return NULL;
}

static void
list_model_iface_init (GListModelInterface *iface)
{
  iface->get_n_items = dzl_directory_model_get_n_items;
  iface->get_item = dzl_directory_model_get_item;
  iface->get_item_type = dzl_directory_model_get_item_type;
}

void
dzl_directory_model_set_visible_func (DzlDirectoryModel            *self,
                                      DzlDirectoryModelVisibleFunc  visible_func,
                                      gpointer                      user_data,
                                      GDestroyNotify                user_data_free_func)
{
  g_return_if_fail (DZL_IS_DIRECTORY_MODEL (self));

  if (self->visible_func_destroy != NULL)
    self->visible_func_destroy (self->visible_func_data);

  self->visible_func = visible_func;
  self->visible_func_data = user_data;
  self->visible_func_destroy = user_data_free_func;

  dzl_directory_model_reload (self);
}
