/* bg-recent-source.c
 *
 * Copyright 2019 Georges Basile Stavracas Neto <georges.stavracas@gmail.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/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "bg-recent-source"

#include "bg-recent-source.h"
#include "cc-background-item.h"

#define ATTRIBUTES G_FILE_ATTRIBUTE_STANDARD_NAME "," \
                   G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," \
                   G_FILE_ATTRIBUTE_TIME_MODIFIED

struct _BgRecentSource
{
  BgSource      parent;

  GFile        *backgrounds_folder;
  GFileMonitor *monitor;

  GCancellable *cancellable;
  GHashTable   *items;
};

G_DEFINE_TYPE (BgRecentSource, bg_recent_source, BG_TYPE_SOURCE)


static const gchar * const content_types[] = {
	"image/png",
	"image/jp2",
	"image/jpeg",
	"image/bmp",
	"image/svg+xml",
	"image/x-portable-anymap",
	NULL
};

static int
sort_func (gconstpointer a,
           gconstpointer b,
           gpointer      user_data)
{
  CcBackgroundItem *item_a;
  CcBackgroundItem *item_b;
  guint64 modified_a;
  guint64 modified_b;
  int retval;

  item_a = (CcBackgroundItem *) a;
  item_b = (CcBackgroundItem *) b;
  modified_a = cc_background_item_get_modified (item_a);
  modified_b = cc_background_item_get_modified (item_b);

  retval = modified_b - modified_a;

  return retval;
}

static void
add_file_from_info (BgRecentSource *self,
                    GFile          *file,
                    GFileInfo      *info)
{
  g_autoptr(CcBackgroundItem) item = NULL;
  CcBackgroundItemFlags flags = 0;
  g_autofree gchar *source_uri = NULL;
  g_autofree gchar *uri = NULL;
  GListStore *store;
  const gchar *content_type;
  guint64 mtime;

  content_type = g_file_info_get_content_type (info);
  mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);

  if (!content_type || !g_strv_contains (content_types, content_type))
    return;

  uri = g_file_get_uri (file);
  item = cc_background_item_new (uri);
  flags |= CC_BACKGROUND_ITEM_HAS_SHADING | CC_BACKGROUND_ITEM_HAS_PLACEMENT;
  g_object_set (G_OBJECT (item),
                "flags", flags,
                "shading", G_DESKTOP_BACKGROUND_SHADING_SOLID,
                "placement", G_DESKTOP_BACKGROUND_STYLE_ZOOM,
                "modified", mtime,
                "needs-download", FALSE,
                "source-url", source_uri,
                NULL);

  store = bg_source_get_liststore (BG_SOURCE (self));
  g_list_store_insert_sorted (store, item, sort_func, self);

  g_hash_table_insert (self->items, g_strdup (uri), g_object_ref (item));
}

static void
remove_item (BgRecentSource   *self,
             CcBackgroundItem *item)
{
  GListStore *store;
  const gchar *uri;
  guint i;

  g_return_if_fail (BG_IS_RECENT_SOURCE (self));
  g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));

  uri = cc_background_item_get_uri (item);
  store = bg_source_get_liststore (BG_SOURCE (self));

  g_debug ("Removing wallpaper %s", uri);

  for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (store)); i++)
    {
      g_autoptr(CcBackgroundItem) tmp = NULL;

      tmp = g_list_model_get_item (G_LIST_MODEL (store), i);

      if (tmp == item)
        {
          g_list_store_remove (store, i);
          break;
        }
    }

  g_hash_table_remove (self->items, cc_background_item_get_uri (item));
}

static void
query_info_finished_cb (GObject      *source,
                        GAsyncResult *result,
                        gpointer      user_data)
{
  BgRecentSource *self;
  g_autoptr(GFileInfo) file_info = NULL;
  g_autoptr(GError) error = NULL;
  GFile *file = NULL;

  file = G_FILE (source);
  file_info = g_file_query_info_finish (file, result, &error);
  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Could not get pictures file information: %s", error->message);
      return;
    }

  self = BG_RECENT_SOURCE (user_data);

  g_debug ("Adding wallpaper %s (%d)",
           g_file_info_get_name (file_info),
           G_IS_FILE (self->backgrounds_folder));

  add_file_from_info (self, file, file_info);
}

static void
on_file_changed_cb (BgRecentSource    *self,
                    GFile             *file,
                    GFile             *other_file,
                    GFileMonitorEvent  event_type)
{
  g_autofree gchar *uri = NULL;

  switch (event_type)
    {
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
      g_file_query_info_async (file,
                               ATTRIBUTES,
                               G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                               G_PRIORITY_DEFAULT,
                               self->cancellable,
                               query_info_finished_cb,
                               self);
      break;

    case G_FILE_MONITOR_EVENT_DELETED:
      uri = g_file_get_uri (file);
      remove_item (self, g_hash_table_lookup (self->items, uri));
      break;

    default:
      return;
    }
}

static int
file_sort_func (gconstpointer a,
                gconstpointer b)
{
  GFileInfo *file_a = G_FILE_INFO (a);
  GFileInfo *file_b = G_FILE_INFO (b);
  guint64 modified_a, modified_b;

  modified_a = g_file_info_get_attribute_uint64 (file_a, G_FILE_ATTRIBUTE_TIME_MODIFIED);
  modified_b = g_file_info_get_attribute_uint64 (file_b, G_FILE_ATTRIBUTE_TIME_MODIFIED);

  return modified_b - modified_a;
}

static void
file_info_async_ready_cb (GObject      *source,
                          GAsyncResult *result,
                          gpointer      user_data)
{
  BgRecentSource *self;
  g_autolist(GFileInfo) file_infos = NULL;
  g_autoptr(GError) error = NULL;
  GFile *parent = NULL;
  GList *l;

  file_infos = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source),
                                                    result,
                                                    &error);
  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Could not get pictures file information: %s", error->message);
      return;
    }

  self = BG_RECENT_SOURCE (user_data);
  parent = g_file_enumerator_get_container (G_FILE_ENUMERATOR (source));

  file_infos = g_list_sort (file_infos, file_sort_func);

  for (l = file_infos; l; l = l->next)
    {
      g_autoptr(GFile) file = NULL;
      GFileInfo *info;

      info = l->data;
      file = g_file_get_child (parent, g_file_info_get_name (info));

      g_debug ("Found recent wallpaper %s", g_file_info_get_name (info));

      add_file_from_info (self, file, info);
    }

  g_file_enumerator_close (G_FILE_ENUMERATOR (source), self->cancellable, &error);

  if (error)
    g_warning ("Error closing file enumerator: %s", error->message);
}

static void
enumerate_children_finished_cb (GObject      *source,
                                GAsyncResult *result,
                                gpointer      user_data)
{
  BgRecentSource *self;
  g_autoptr(GFileEnumerator) enumerator = NULL;
  g_autoptr(GError) error = NULL;

  enumerator = g_file_enumerate_children_finish (G_FILE (source), result, &error);

  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Could not fill pictures source: %s", error->message);
      return;
    }

  self = BG_RECENT_SOURCE (user_data);
  g_file_enumerator_next_files_async (enumerator,
                                      G_MAXINT,
                                      G_PRIORITY_DEFAULT,
                                      self->cancellable,
                                      file_info_async_ready_cb,
                                      self);
}

static void
load_backgrounds (BgRecentSource *self)
{
  g_autofree gchar *backgrounds_path = NULL;
  g_autoptr(GError) error = NULL;

  if (!g_file_make_directory_with_parents (self->backgrounds_folder, self->cancellable, &error) &&
      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
    {
      g_critical ("Failed to create local background directory: %s", error->message);
      return;
    }

  backgrounds_path = g_file_get_path (self->backgrounds_folder);
  g_debug ("Enumerating wallpapers under %s", backgrounds_path);

  g_file_enumerate_children_async (self->backgrounds_folder,
                                   ATTRIBUTES,
                                   G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                   G_PRIORITY_DEFAULT,
                                   self->cancellable,
                                   enumerate_children_finished_cb,
                                   self);

  self->monitor = g_file_monitor_directory (self->backgrounds_folder,
                                            G_FILE_MONITOR_WATCH_MOVES,
                                            self->cancellable,
                                            &error);

  if (!self->monitor)
    {
      g_critical ("Failed to monitor background directory: %s", error->message);
      return;
    }

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

/* Callbacks */

static void
on_file_copied_cb (GObject      *source,
                   GAsyncResult *result,
                   gpointer      user_data)
{
  g_autoptr(BgRecentSource) self = BG_RECENT_SOURCE (user_data);
  g_autofree gchar *original_file = NULL;
  g_autoptr(GError) error = NULL;

  g_file_copy_finish (G_FILE (source), result, &error);

  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_critical ("Failed to copy file: %s", error->message);
      return;
    }

   original_file = g_file_get_path (G_FILE (source));
   g_debug ("Successfully copied wallpaper: %s", original_file);
}

static void
on_file_deleted_cb (GObject      *source,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  g_autoptr(BgRecentSource) self = BG_RECENT_SOURCE (user_data);
  g_autofree gchar *original_file = NULL;
  g_autoptr(GError) error = NULL;

  g_file_delete_finish (G_FILE (source), result, &error);

  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_critical ("Failed to delete wallpaper: %s", error->message);
      return;
    }

  original_file = g_file_get_path (G_FILE (source));
  g_debug ("Successfully deleted wallpaper: %s", original_file);
}

/* GObject overrides */

static void
bg_recent_source_finalize (GObject *object)
{
  BgRecentSource *self = (BgRecentSource *)object;

  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);
  g_clear_object (&self->monitor);

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

static void
bg_recent_source_class_init (BgRecentSourceClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = bg_recent_source_finalize;
}

static void
bg_recent_source_init (BgRecentSource *self)
{
  g_autofree gchar *backgrounds_path = NULL;

  backgrounds_path = g_build_filename (g_get_user_data_dir (), "backgrounds", NULL);

  self->items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
  self->cancellable = g_cancellable_new ();
  self->backgrounds_folder = g_file_new_for_path (backgrounds_path);

  load_backgrounds (self);
}

BgRecentSource*
bg_recent_source_new (GtkWidget *widget)
{
  return g_object_new (BG_TYPE_RECENT_SOURCE,
                       "widget", widget,
                       NULL);
}

void
bg_recent_source_add_file (BgRecentSource *self,
                           const gchar    *path)
{
  g_autoptr(GDateTime) now = NULL;
  g_autofree gchar *destination_name = NULL;
  g_autofree gchar *formatted_now = NULL;
  g_autofree gchar *basename = NULL;
  g_autoptr(GFile) destination = NULL;
  g_autoptr(GFile) file = NULL;

  g_return_if_fail (BG_IS_RECENT_SOURCE (self));
  g_return_if_fail (path && *path);

  g_debug ("Importing wallpaper %s", path);

  now = g_date_time_new_now_local ();
  formatted_now = g_date_time_format (now, "%Y-%m-%d-%H-%M-%S");

  file = g_file_new_for_path (path);

  basename = g_file_get_basename (file);
  destination_name = g_strdup_printf ("%s-%s", formatted_now, basename);
  destination = g_file_get_child (self->backgrounds_folder, destination_name);

  g_file_copy_async (file,
                     destination,
                     G_FILE_COPY_NONE,
                     G_PRIORITY_DEFAULT,
                     self->cancellable,
                     NULL, NULL,
                     on_file_copied_cb,
                     g_object_ref (self));
}

void
bg_recent_source_remove_item (BgRecentSource   *self,
                              CcBackgroundItem *item)
{
  g_autoptr(GFile) file = NULL;
  const gchar *uri;

  g_return_if_fail (BG_IS_RECENT_SOURCE (self));
  g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));

  uri = cc_background_item_get_uri (item);
  file = g_file_new_for_uri (uri);

  g_file_delete_async (file,
                       G_PRIORITY_DEFAULT,
                       self->cancellable,
                       on_file_deleted_cb,
                       g_object_ref (self));
}
