/* nautilus-favorite-directory.c
 *
 * Copyright (C) 2017 Alexandru Pandelea <alexandru.pandelea@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 2 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/>.
 */

#include "nautilus-favorite-directory.h"
#include "nautilus-tag-manager.h"
#include "nautilus-file-utilities.h"
#include "nautilus-directory-private.h"
#include <glib/gi18n.h>

struct _NautilusFavoriteDirectory {
    NautilusDirectory parent_slot;

    NautilusTagManager *tag_manager;
    GList *files;

    GList *monitor_list;
    GList *callback_list;
    GList *pending_callback_list;
};

typedef struct
{
    gboolean monitor_hidden_files;
    NautilusFileAttributes monitor_attributes;

    gconstpointer client;
} FavoriteMonitor;

typedef struct
{
    NautilusFavoriteDirectory *favorite_directory;

    NautilusDirectoryCallback callback;
    gpointer callback_data;

    NautilusFileAttributes wait_for_attributes;
    gboolean wait_for_file_list;
    GList *file_list;
} FavoriteCallback;

G_DEFINE_TYPE_WITH_CODE (NautilusFavoriteDirectory, nautilus_favorite_directory, NAUTILUS_TYPE_DIRECTORY,
                         nautilus_ensure_extension_points ();
                         g_io_extension_point_implement (NAUTILUS_DIRECTORY_PROVIDER_EXTENSION_POINT_NAME,
                                                         g_define_type_id,
                                                         NAUTILUS_FAVORITE_DIRECTORY_PROVIDER_NAME,
                                                         0));

static void
file_changed (NautilusFile              *file,
              NautilusFavoriteDirectory *favorite)
{
    GList list;

    list.data = file;
    list.next = NULL;

    nautilus_directory_emit_files_changed (NAUTILUS_DIRECTORY (favorite), &list);
}

static void
nautilus_favorite_directory_update_files (NautilusFavoriteDirectory *self)
{
    GList *l;
    GList *tmp_l;
    GList *new_favorite_files;
    GList *monitor_list;
    FavoriteMonitor *monitor;
    NautilusFile *file;
    GHashTable *uri_table;
    GList *files_added;
    GList *files_removed;
    gchar *uri;

    files_added = NULL;
    files_removed = NULL;

    uri_table = g_hash_table_new_full (g_str_hash,
                                       g_str_equal,
                                       (GDestroyNotify) g_free,
                                       NULL);

    for (l = self->files; l != NULL; l = l->next)
    {
        g_hash_table_add (uri_table, nautilus_file_get_uri (NAUTILUS_FILE (l->data)));
    }

    new_favorite_files = nautilus_tag_manager_get_favorite_files (self->tag_manager);

    for (l = new_favorite_files; l != NULL; l = l->next)
    {
        if (!g_hash_table_contains (uri_table, l->data))
        {
            file = nautilus_file_get_by_uri ((gchar*) l->data);

            for (monitor_list = self->monitor_list; monitor_list; monitor_list = monitor_list->next)
            {
                monitor = monitor_list->data;

                /* Add monitors */
                nautilus_file_monitor_add (file, monitor, monitor->monitor_attributes);
            }

            g_signal_connect (file, "changed", G_CALLBACK (file_changed), self);

            files_added = g_list_prepend (files_added, file);
        }
    }

    l = self->files;
    while (l != NULL)
    {
        uri = nautilus_file_get_uri (NAUTILUS_FILE (l->data));

        if (!nautilus_tag_manager_file_is_favorite (self->tag_manager, uri))
        {
            files_removed = g_list_prepend (files_removed,
                                            nautilus_file_ref (NAUTILUS_FILE (l->data)));

            g_signal_handlers_disconnect_by_func (NAUTILUS_FILE (l->data),
                                                  file_changed,
                                                  self);

            /* Remove monitors */
            for (monitor_list = self->monitor_list; monitor_list;
                 monitor_list = monitor_list->next)
            {
                monitor = monitor_list->data;
                nautilus_file_monitor_remove (NAUTILUS_FILE (l->data), monitor);
            }

            if (l == self->files)
            {
                self->files = g_list_delete_link (self->files, l);
                l = self->files;
            }
            else
            {
                tmp_l = l->prev;
                self->files = g_list_delete_link (self->files, l);
                l = tmp_l->next;
            }
        }
        else
        {
            l = l->next;
        }

        g_free (uri);
    }

    if (files_added)
    {
        nautilus_directory_emit_files_added (NAUTILUS_DIRECTORY (self), files_added);

        for (l = files_added; l != NULL; l = l->next)
        {
            self->files = g_list_prepend (self->files, nautilus_file_ref (NAUTILUS_FILE (l->data)));
        }
    }

    if (files_removed)
    {
        nautilus_directory_emit_files_changed (NAUTILUS_DIRECTORY (self), files_removed);
    }

    nautilus_file_list_free (files_added);
    nautilus_file_list_free (files_removed);
    g_hash_table_destroy (uri_table);
}

static void
on_favorites_files_changed (NautilusTagManager        *tag_manager,
                            GList                     *changed_files,
                            gpointer                   user_data)
{
    NautilusFavoriteDirectory *self;

    self = NAUTILUS_FAVORITE_DIRECTORY (user_data);

    nautilus_favorite_directory_update_files (self);
}

static gboolean
real_contains_file (NautilusDirectory *directory,
                    NautilusFile      *file)
{
    NautilusFavoriteDirectory *self;
    g_autofree gchar *uri = NULL;

    self = NAUTILUS_FAVORITE_DIRECTORY (directory);

    uri = nautilus_file_get_uri (file);

    return nautilus_tag_manager_file_is_favorite (self->tag_manager, uri);
}

static gboolean
real_is_editable (NautilusDirectory *directory)
{
    return FALSE;
}

static void
real_force_reload (NautilusDirectory *directory)
{
    nautilus_favorite_directory_update_files (NAUTILUS_FAVORITE_DIRECTORY (directory));
}

static void
real_call_when_ready (NautilusDirectory         *directory,
                      NautilusFileAttributes     file_attributes,
                      gboolean                   wait_for_file_list,
                      NautilusDirectoryCallback  callback,
                      gpointer                   callback_data)
{
    GList *file_list;
    NautilusFavoriteDirectory *favorite;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (directory);

    file_list = nautilus_file_list_copy (favorite->files);

    callback (NAUTILUS_DIRECTORY (directory),
                                  file_list,
                                  callback_data);
}

static gboolean
real_are_all_files_seen (NautilusDirectory *directory)
{
    return TRUE;
}

static void
real_file_monitor_add (NautilusDirectory         *directory,
                       gconstpointer              client,
                       gboolean                   monitor_hidden_files,
                       NautilusFileAttributes     file_attributes,
                       NautilusDirectoryCallback  callback,
                       gpointer                   callback_data)
{
    GList *list;
    FavoriteMonitor *monitor;
    NautilusFavoriteDirectory *favorite;
    NautilusFile *file;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (directory);

    monitor = g_new0 (FavoriteMonitor, 1);
    monitor->monitor_hidden_files = monitor_hidden_files;
    monitor->monitor_attributes = file_attributes;
    monitor->client = client;

    favorite->monitor_list = g_list_prepend (favorite->monitor_list, monitor);

    if (callback != NULL)
    {
        (*callback) (directory, favorite->files, callback_data);
    }

    for (list = favorite->files; list != NULL; list = list->next)
    {
        file = list->data;

        /* Add monitors */
        nautilus_file_monitor_add (file, monitor, file_attributes);
    }
}

static void
favorite_monitor_destroy (FavoriteMonitor           *monitor,
                          NautilusFavoriteDirectory *favorite)
{
    GList *l;
    NautilusFile *file;

    for (l = favorite->files; l != NULL; l = l->next)
    {
        file = l->data;

        nautilus_file_monitor_remove (file, monitor);
    }

    g_free (monitor);
}

static void
real_monitor_remove (NautilusDirectory *directory,
                     gconstpointer      client)
{
    NautilusFavoriteDirectory *favorite;
    FavoriteMonitor *monitor;
    GList *list;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (directory);

    for (list = favorite->monitor_list; list != NULL; list = list->next)
    {
        monitor = list->data;

        if (monitor->client != client)
            continue;

        favorite->monitor_list = g_list_delete_link (favorite->monitor_list, list);

        favorite_monitor_destroy (monitor, favorite);

        break;
    }
}

static gboolean
real_handles_location (GFile *location)
{
    g_autofree gchar *uri = NULL;

    uri = g_file_get_uri (location);

    if (eel_uri_is_favorites (uri))
    {
        return TRUE;
    }

    return FALSE;
}

static FavoriteCallback*
favorite_callback_find_pending (NautilusFavoriteDirectory *favorite,
                                NautilusDirectoryCallback  callback,
                                gpointer                   callback_data)
{
    FavoriteCallback *favorite_callback;
    GList *list;

    for (list = favorite->pending_callback_list; list != NULL; list = list->next)
    {
        favorite_callback = list->data;

        if (favorite_callback->callback == callback &&
            favorite_callback->callback_data == callback_data)
        {
            return favorite_callback;
        }
    }

    return NULL;
}

static FavoriteCallback*
favorite_callback_find (NautilusFavoriteDirectory *favorite,
                        NautilusDirectoryCallback  callback,
                        gpointer                   callback_data)
{
    FavoriteCallback *favorite_callback;
    GList *list;

    for (list = favorite->callback_list; list != NULL; list = list->next)
    {
        favorite_callback = list->data;

        if (favorite_callback->callback == callback &&
            favorite_callback->callback_data == callback_data)
        {
            return favorite_callback;
        }
    }

    return NULL;
}

static void
favorite_callback_destroy (FavoriteCallback *favorite_callback)
{
    nautilus_file_list_free (favorite_callback->file_list);

    g_free (favorite_callback);
}

static void
real_cancel_callback (NautilusDirectory        *directory,
                      NautilusDirectoryCallback callback,
                      gpointer                  callback_data)
{
    NautilusFavoriteDirectory *favorite;
    FavoriteCallback *favorite_callback;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (directory);
    favorite_callback = favorite_callback_find (favorite, callback, callback_data);

    if (favorite_callback)
    {
        favorite->callback_list = g_list_remove (favorite->callback_list, favorite_callback);

        favorite_callback_destroy (favorite_callback);

        return;
    }

    /* Check for a pending callback */
    favorite_callback = favorite_callback_find_pending (favorite, callback, callback_data);

    if (favorite_callback)
    {
        favorite->pending_callback_list = g_list_remove (favorite->pending_callback_list, favorite_callback);

        favorite_callback_destroy (favorite_callback);
    }
}

static GList*
real_get_file_list (NautilusDirectory *directory)
{
    NautilusFavoriteDirectory *favorite;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (directory);

    return nautilus_file_list_copy (favorite->files);
}

static void
nautilus_favorite_directory_set_files (NautilusFavoriteDirectory *self)
{
    GList *favorite_files;
    NautilusFile *file;
    GList *l;
    GList *file_list;
    FavoriteMonitor *monitor;
    GList *monitor_list;

    file_list = NULL;

    favorite_files = nautilus_tag_manager_get_favorite_files (self->tag_manager);

    for (l = favorite_files; l != NULL; l = l->next)
    {
        file = nautilus_file_get_by_uri ((gchar*) l->data);

        g_signal_connect (file, "changed", G_CALLBACK (file_changed), self);

        for (monitor_list = self->monitor_list; monitor_list; monitor_list = monitor_list->next)
        {
            monitor = monitor_list->data;

            /* Add monitors */
            nautilus_file_monitor_add (file, monitor, monitor->monitor_attributes);
        }

        file_list = g_list_prepend (file_list, file);
    }

    nautilus_directory_emit_files_added (NAUTILUS_DIRECTORY (self), file_list);

    self->files = file_list;
}

static void
nautilus_favorite_directory_finalize (GObject *object)
{
    NautilusFavoriteDirectory *self;

    self = NAUTILUS_FAVORITE_DIRECTORY (object);

    g_signal_handlers_disconnect_by_func (self->tag_manager,
                                          on_favorites_files_changed,
                                          self);

    g_object_unref (self->tag_manager);
    nautilus_file_list_free (self->files);

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

static void
nautilus_favorite_directory_dispose (GObject *object)
{
    NautilusFavoriteDirectory *favorite;
    GList *l;
    GList *monitor_list;
    FavoriteMonitor *monitor;
    NautilusFile *file;

    favorite = NAUTILUS_FAVORITE_DIRECTORY (object);

    /* Remove file connections */
    for (l = favorite->files; l != NULL; l = l->next)
    {
        file = l->data;

        /* Disconnect change handler */
        g_signal_handlers_disconnect_by_func (file, file_changed, favorite);

        /* Remove monitors */
        for (monitor_list = favorite->monitor_list; monitor_list;
             monitor_list = monitor_list->next)
        {
            monitor = monitor_list->data;
            nautilus_file_monitor_remove (file, monitor);
        }
    }

    /* Remove search monitors */
    if (favorite->monitor_list)
    {
        for (l = favorite->monitor_list; l != NULL; l = l->next)
        {
            favorite_monitor_destroy ((FavoriteMonitor*) l->data, favorite);
        }

        g_list_free (favorite->monitor_list);
        favorite->monitor_list = NULL;
    }

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

static void
nautilus_favorite_directory_class_init (NautilusFavoriteDirectoryClass *klass)
{
    GObjectClass *oclass;
    NautilusDirectoryClass *directory_class;

    oclass = G_OBJECT_CLASS (klass);
    directory_class = NAUTILUS_DIRECTORY_CLASS (klass);

    oclass->finalize = nautilus_favorite_directory_finalize;
    oclass->dispose = nautilus_favorite_directory_dispose;

    directory_class->handles_location = real_handles_location;
    directory_class->contains_file = real_contains_file;
    directory_class->is_editable = real_is_editable;
    directory_class->force_reload = real_force_reload;
    directory_class->call_when_ready = real_call_when_ready;
    directory_class->are_all_files_seen = real_are_all_files_seen;
    directory_class->file_monitor_add = real_file_monitor_add;
    directory_class->file_monitor_remove = real_monitor_remove;
    directory_class->cancel_callback = real_cancel_callback;
    directory_class->get_file_list = real_get_file_list;
}

NautilusFavoriteDirectory*
nautilus_favorite_directory_new ()
{
    NautilusFavoriteDirectory *self;

    self = g_object_new (NAUTILUS_TYPE_FAVORITE_DIRECTORY, NULL);

    return self;
}

static void
nautilus_favorite_directory_init (NautilusFavoriteDirectory *self)
{
    NautilusTagManager *tag_manager;

    tag_manager = nautilus_tag_manager_get ();

    g_signal_connect (tag_manager,
                      "favorites-changed",
                      (GCallback) on_favorites_files_changed,
                      self);

    self->tag_manager = tag_manager;

    nautilus_favorite_directory_set_files (self);

}
