/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-

   caja-merged-directory.c: Subclass of CajaDirectory to implement the
   virtual merged directory.

   Copyright (C) 1999, 2000 Eazel, Inc.

   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, write to the
   Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
   Boston, MA 02110-1301, USA.

   Author: Darin Adler <darin@bentspoon.com>
*/

#include <config.h>
#include "caja-merged-directory.h"

#include "caja-directory-private.h"
#include "caja-directory-notify.h"
#include "caja-file.h"
#include <eel/eel-glib-extensions.h>
#include <eel/eel-gtk-macros.h>
#include <gtk/gtk.h>

#include <src/glibcompat.h> /* for g_list_free_full */

struct CajaMergedDirectoryDetails
{
    GList *directories;
    GList *directories_not_done_loading;
    GHashTable *callbacks;
    GHashTable *monitors;
};

typedef struct
{
    CajaMergedDirectory *merged;
    CajaDirectoryCallback callback;
    gpointer callback_data;

    CajaFileAttributes wait_for_attributes;
    gboolean wait_for_file_list;

    GList *non_ready_directories;
    GList *merged_file_list;
} MergedCallback;

typedef struct
{
    CajaMergedDirectory *merged;

    gboolean monitor_hidden_files;
    CajaFileAttributes monitor_attributes;
} MergedMonitor;

enum
{
    ADD_REAL_DIRECTORY,
    REMOVE_REAL_DIRECTORY,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

G_DEFINE_TYPE (CajaMergedDirectory, caja_merged_directory,
               CAJA_TYPE_DIRECTORY);
#define parent_class caja_merged_directory_parent_class

static guint
merged_callback_hash (gconstpointer merged_callback_as_pointer)
{
    const MergedCallback *merged_callback;

    merged_callback = merged_callback_as_pointer;
    return GPOINTER_TO_UINT (merged_callback->callback)
           ^ GPOINTER_TO_UINT (merged_callback->callback_data);
}

static gboolean
merged_callback_equal (gconstpointer merged_callback_as_pointer,
                       gconstpointer merged_callback_as_pointer_2)
{
    const MergedCallback *merged_callback, *merged_callback_2;

    merged_callback = merged_callback_as_pointer;
    merged_callback_2 = merged_callback_as_pointer_2;

    return merged_callback->callback == merged_callback_2->callback
           && merged_callback->callback_data == merged_callback_2->callback_data;
}

static void
merged_callback_destroy (MergedCallback *merged_callback)
{
    g_assert (merged_callback != NULL);
    g_assert (CAJA_IS_MERGED_DIRECTORY (merged_callback->merged));

    g_list_free (merged_callback->non_ready_directories);
    caja_file_list_free (merged_callback->merged_file_list);
    g_free (merged_callback);
}

static void
merged_callback_check_done (MergedCallback *merged_callback)
{
    /* Check if we are ready. */
    if (merged_callback->non_ready_directories != NULL)
    {
        return;
    }

    /* Remove from the hash table before sending it. */
    g_hash_table_remove (merged_callback->merged->details->callbacks, merged_callback);

    /* We are ready, so do the real callback. */
    (* merged_callback->callback) (CAJA_DIRECTORY (merged_callback->merged),
                                   merged_callback->merged_file_list,
                                   merged_callback->callback_data);

    /* And we are done. */
    merged_callback_destroy (merged_callback);
}

static void
merged_callback_remove_directory (MergedCallback *merged_callback,
                                  CajaDirectory *directory)
{
    merged_callback->non_ready_directories = g_list_remove
            (merged_callback->non_ready_directories, directory);
    merged_callback_check_done (merged_callback);
}

static void
directory_ready_callback (CajaDirectory *directory,
                          GList *files,
                          gpointer callback_data)
{
    MergedCallback *merged_callback;

    g_assert (CAJA_IS_DIRECTORY (directory));
    g_assert (callback_data != NULL);

    merged_callback = callback_data;
    g_assert (g_list_find (merged_callback->non_ready_directories, directory) != NULL);

    /* Update based on this call. */
    merged_callback->merged_file_list = g_list_concat
                                        (merged_callback->merged_file_list,
                                         caja_file_list_copy (files));

    /* Check if we are ready. */
    merged_callback_remove_directory (merged_callback, directory);
}

static void
merged_call_when_ready (CajaDirectory *directory,
                        CajaFileAttributes file_attributes,
                        gboolean wait_for_file_list,
                        CajaDirectoryCallback callback,
                        gpointer callback_data)
{
    CajaMergedDirectory *merged;
    MergedCallback search_key, *merged_callback;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    /* Check to be sure we aren't overwriting. */
    search_key.callback = callback;
    search_key.callback_data = callback_data;
    if (g_hash_table_lookup (merged->details->callbacks, &search_key) != NULL)
    {
        g_warning ("tried to add a new callback while an old one was pending");
        return;
    }

    /* Create a merged_callback record. */
    merged_callback = g_new0 (MergedCallback, 1);
    merged_callback->merged = merged;
    merged_callback->callback = callback;
    merged_callback->callback_data = callback_data;
    merged_callback->wait_for_attributes = file_attributes;
    merged_callback->wait_for_file_list = wait_for_file_list;
    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        merged_callback->non_ready_directories = g_list_prepend
                (merged_callback->non_ready_directories, node->data);
    }

    /* Put it in the hash table. */
    g_hash_table_insert (merged->details->callbacks,
                         merged_callback, merged_callback);

    /* Handle the pathological case where there are no directories. */
    if (merged->details->directories == NULL)
    {
        merged_callback_check_done (merged_callback);
    }

    /* Now tell all the directories about it. */
    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        caja_directory_call_when_ready
        (node->data,
         merged_callback->wait_for_attributes,
         merged_callback->wait_for_file_list,
         directory_ready_callback, merged_callback);
    }
}

static void
merged_cancel_callback (CajaDirectory *directory,
                        CajaDirectoryCallback callback,
                        gpointer callback_data)
{
    CajaMergedDirectory *merged;
    MergedCallback search_key, *merged_callback;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    /* Find the entry in the table. */
    search_key.callback = callback;
    search_key.callback_data = callback_data;
    merged_callback = g_hash_table_lookup (merged->details->callbacks, &search_key);
    if (merged_callback == NULL)
    {
        return;
    }

    /* Remove from the hash table before working with it. */
    g_hash_table_remove (merged_callback->merged->details->callbacks, merged_callback);

    /* Tell all the directories to cancel the call. */
    for (node = merged_callback->non_ready_directories; node != NULL; node = node->next)
    {
        caja_directory_cancel_callback
        (node->data,
         directory_ready_callback, merged_callback);
    }
    merged_callback_destroy (merged_callback);
}

static void
build_merged_callback_list (CajaDirectory *directory,
                            GList *file_list,
                            gpointer callback_data)
{
    GList **merged_list;

    merged_list = callback_data;
    *merged_list = g_list_concat (*merged_list,
                                  caja_file_list_copy (file_list));
}

/* Create a monitor on each of the directories in the list. */
static void
merged_monitor_add (CajaDirectory *directory,
                    gconstpointer client,
                    gboolean monitor_hidden_files,
                    CajaFileAttributes file_attributes,
                    CajaDirectoryCallback callback,
                    gpointer callback_data)
{
    CajaMergedDirectory *merged;
    MergedMonitor *monitor;
    GList *node;
    GList *merged_callback_list;

    merged = CAJA_MERGED_DIRECTORY (directory);

    /* Map the client to a unique value so this doesn't interfere
     * with direct monitoring of the directory by the same client.
     */
    monitor = g_hash_table_lookup (merged->details->monitors, client);
    if (monitor != NULL)
    {
        g_assert (monitor->merged == merged);
    }
    else
    {
        monitor = g_new0 (MergedMonitor, 1);
        monitor->merged = merged;
        g_hash_table_insert (merged->details->monitors,
                             (gpointer) client, monitor);
    }
    monitor->monitor_hidden_files = monitor_hidden_files;
    monitor->monitor_attributes = file_attributes;

    /* Call through to the real directory add calls. */
    merged_callback_list = NULL;
    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        caja_directory_file_monitor_add
        (node->data, monitor,
         monitor_hidden_files,
         file_attributes,
         build_merged_callback_list, &merged_callback_list);
    }
    if (callback != NULL)
    {
        (* callback) (directory, merged_callback_list, callback_data);
    }
    caja_file_list_free (merged_callback_list);
}

static void
merged_monitor_destroy (CajaMergedDirectory *merged, MergedMonitor *monitor)
{
    GList *node;

    /* Call through to the real directory remove calls. */
    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        caja_directory_file_monitor_remove (node->data, monitor);
    }

    g_free (monitor);
}

/* Remove the monitor from each of the directories in the list. */
static void
merged_monitor_remove (CajaDirectory *directory,
                       gconstpointer client)
{
    CajaMergedDirectory *merged;
    MergedMonitor *monitor;

    merged = CAJA_MERGED_DIRECTORY (directory);

    /* Map the client to the value used by the earlier add call. */
    monitor = g_hash_table_lookup (merged->details->monitors, client);
    if (monitor == NULL)
    {
        return;
    }
    g_hash_table_remove (merged->details->monitors, client);

    merged_monitor_destroy (merged, monitor);
}

static void
merged_force_reload (CajaDirectory *directory)
{
    CajaMergedDirectory *merged;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    /* Call through to the real force_reload calls. */
    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        caja_directory_force_reload (node->data);
    }
}

/* Return true if any directory in the list does. */
static gboolean
merged_contains_file (CajaDirectory *directory,
                      CajaFile *file)
{
    CajaMergedDirectory *merged;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        if (caja_directory_contains_file (node->data, file))
        {
            return TRUE;
        }
    }
    return FALSE;
}

/* Return true only if all directories in the list do. */
static gboolean
merged_are_all_files_seen (CajaDirectory *directory)
{
    CajaMergedDirectory *merged;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        if (!caja_directory_are_all_files_seen (node->data))
        {
            return FALSE;
        }
    }
    return TRUE;
}

/* Return true if any directory in the list does. */
static gboolean
merged_is_not_empty (CajaDirectory *directory)
{
    CajaMergedDirectory *merged;
    GList *node;

    merged = CAJA_MERGED_DIRECTORY (directory);

    for (node = merged->details->directories; node != NULL; node = node->next)
    {
        if (caja_directory_is_not_empty (node->data))
        {
            return TRUE;
        }
    }
    return FALSE;
}

static GList *
merged_get_file_list (CajaDirectory *directory)
{
    GList *dirs_file_list, *merged_dir_file_list = NULL;
    GList *dir_list;
    GList *cur_node;

    dirs_file_list = NULL;
    dir_list = CAJA_MERGED_DIRECTORY (directory)->details->directories;

    for (cur_node = dir_list; cur_node != NULL; cur_node = cur_node->next)
    {
        CajaDirectory *cur_dir;

        cur_dir = CAJA_DIRECTORY (cur_node->data);
        dirs_file_list = g_list_concat (dirs_file_list,
                                        caja_directory_get_file_list (cur_dir));
    }

    merged_dir_file_list = EEL_CALL_PARENT_WITH_RETURN_VALUE
                           (CAJA_DIRECTORY_CLASS, get_file_list, (directory));

    return g_list_concat (dirs_file_list, merged_dir_file_list);
}

static void
forward_files_added_cover (CajaDirectory *real_directory,
                           GList *files,
                           gpointer callback_data)
{
    caja_directory_emit_files_added (CAJA_DIRECTORY (callback_data), files);
}

static void
forward_files_changed_cover (CajaDirectory *real_directory,
                             GList *files,
                             gpointer callback_data)
{
    caja_directory_emit_files_changed (CAJA_DIRECTORY (callback_data), files);
}

static void
done_loading_callback (CajaDirectory *real_directory,
                       CajaMergedDirectory *merged)
{
    merged->details->directories_not_done_loading = g_list_remove
            (merged->details->directories_not_done_loading, real_directory);
    if (merged->details->directories_not_done_loading == NULL)
    {
        caja_directory_emit_done_loading (CAJA_DIRECTORY (merged));
    }
}

static void
monitor_add_directory (gpointer key,
                       gpointer value,
                       gpointer callback_data)
{
    MergedMonitor *monitor;

    monitor = value;
    caja_directory_file_monitor_add
    (CAJA_DIRECTORY (callback_data), monitor,
     monitor->monitor_hidden_files,
     monitor->monitor_attributes,
     forward_files_added_cover, monitor->merged);
}

static void
merged_add_real_directory (CajaMergedDirectory *merged,
                           CajaDirectory *real_directory)
{
    g_return_if_fail (CAJA_IS_MERGED_DIRECTORY (merged));
    g_return_if_fail (CAJA_IS_DIRECTORY (real_directory));
    g_return_if_fail (!CAJA_IS_MERGED_DIRECTORY (real_directory));
    g_return_if_fail (g_list_find (merged->details->directories, real_directory) == NULL);

    /* Add to our list of directories. */
    caja_directory_ref (real_directory);
    merged->details->directories = g_list_prepend
                                   (merged->details->directories, real_directory);
    merged->details->directories_not_done_loading = g_list_prepend
            (merged->details->directories_not_done_loading, real_directory);

    g_signal_connect_object (real_directory, "done_loading",
                             G_CALLBACK (done_loading_callback), merged, 0);

    /* FIXME bugzilla.gnome.org 45084: The done_loading part won't work for the case where
         * we have no directories in our list.
     */

    /* Add the directory to any extant monitors. */
    g_hash_table_foreach (merged->details->monitors,
                          monitor_add_directory,
                          real_directory);
    /* FIXME bugzilla.gnome.org 42541: Do we need to add the directory to callbacks too? */

    g_signal_connect_object (real_directory, "files_added",
                             G_CALLBACK (forward_files_added_cover), merged, 0);
    g_signal_connect_object (real_directory, "files_changed",
                             G_CALLBACK (forward_files_changed_cover), merged, 0);
}

void
caja_merged_directory_add_real_directory (CajaMergedDirectory *merged,
        CajaDirectory *real_directory)
{
    g_return_if_fail (CAJA_IS_MERGED_DIRECTORY (merged));
    g_return_if_fail (CAJA_IS_DIRECTORY (real_directory));
    g_return_if_fail (!CAJA_IS_MERGED_DIRECTORY (real_directory));

    /* Quietly do nothing if asked to add something that's already there. */
    if (g_list_find (merged->details->directories, real_directory) != NULL)
    {
        return;
    }

    g_signal_emit (merged, signals[ADD_REAL_DIRECTORY], 0, real_directory);
}

GList *
caja_merged_directory_get_real_directories (CajaMergedDirectory *merged)
{
    return g_list_copy (merged->details->directories);
}

static void
merged_callback_remove_directory_cover (gpointer key,
                                        gpointer value,
                                        gpointer callback_data)
{
    merged_callback_remove_directory
    (value, CAJA_DIRECTORY (callback_data));
}

static void
monitor_remove_directory (gpointer key,
                          gpointer value,
                          gpointer callback_data)
{
    caja_directory_file_monitor_remove
    (CAJA_DIRECTORY (callback_data), value);
}

static void
real_directory_notify_files_removed (CajaDirectory *real_directory)
{
    GList *files, *l;

    files = caja_directory_get_file_list (real_directory);

    for (l = files; l; l = l->next)
    {
        CajaFile *file;
        char *uri;

        file = CAJA_FILE (l->data);
        uri = caja_file_get_uri (file);
        caja_file_unref (file);

        l->data = uri;
    }

    if (files)
    {
        caja_directory_notify_files_removed_by_uri (files);
    }

    g_list_free_full (files, g_free);
}

static void
merged_remove_real_directory (CajaMergedDirectory *merged,
                              CajaDirectory *real_directory)
{
    g_return_if_fail (CAJA_IS_MERGED_DIRECTORY (merged));
    g_return_if_fail (CAJA_IS_DIRECTORY (real_directory));
    g_return_if_fail (g_list_find (merged->details->directories, real_directory) != NULL);

    /* Since the real directory will be going away, act as if files were removed */
    real_directory_notify_files_removed (real_directory);

    /* Remove this directory from callbacks and monitors. */
    eel_g_hash_table_safe_for_each (merged->details->callbacks,
                                    merged_callback_remove_directory_cover,
                                    real_directory);
    g_hash_table_foreach (merged->details->monitors,
                          monitor_remove_directory,
                          real_directory);

    /* Disconnect all the signals. */
    g_signal_handlers_disconnect_matched
    (real_directory, G_SIGNAL_MATCH_DATA,
     0, 0, NULL, NULL, merged);

    /* Remove from our list of directories. */
    merged->details->directories = g_list_remove
                                   (merged->details->directories, real_directory);
    merged->details->directories_not_done_loading = g_list_remove
            (merged->details->directories_not_done_loading, real_directory);
    caja_directory_unref (real_directory);
}

void
caja_merged_directory_remove_real_directory (CajaMergedDirectory *merged,
        CajaDirectory *real_directory)
{
    g_return_if_fail (CAJA_IS_MERGED_DIRECTORY (merged));

    /* Quietly do nothing if asked to remove something that's not there. */
    if (g_list_find (merged->details->directories, real_directory) == NULL)
    {
        return;
    }

    g_signal_emit (merged, signals[REMOVE_REAL_DIRECTORY], 0, real_directory);
}

static void
merged_monitor_destroy_cover (gpointer key,
                              gpointer value,
                              gpointer callback_data)
{
    merged_monitor_destroy (callback_data, value);
}

static void
merged_callback_destroy_cover (gpointer key,
                               gpointer value,
                               gpointer callback_data)
{
    merged_callback_destroy (value);
}

static void
merged_finalize (GObject *object)
{
    CajaMergedDirectory *merged;

    merged = CAJA_MERGED_DIRECTORY (object);

    g_hash_table_foreach (merged->details->monitors,
                          merged_monitor_destroy_cover, merged);
    g_hash_table_foreach (merged->details->callbacks,
                          merged_callback_destroy_cover, NULL);

    g_hash_table_destroy (merged->details->callbacks);
    g_hash_table_destroy (merged->details->monitors);
    caja_directory_list_free (merged->details->directories);
    g_list_free (merged->details->directories_not_done_loading);
    g_free (merged->details);

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

static void
caja_merged_directory_init (CajaMergedDirectory *merged)
{
    merged->details = g_new0 (CajaMergedDirectoryDetails, 1);
    merged->details->callbacks = g_hash_table_new
                                 (merged_callback_hash, merged_callback_equal);
    merged->details->monitors = g_hash_table_new (NULL, NULL);
}

static void
caja_merged_directory_class_init (CajaMergedDirectoryClass *class)
{
    CajaDirectoryClass *directory_class;

    directory_class = CAJA_DIRECTORY_CLASS (class);

    G_OBJECT_CLASS (class)->finalize = merged_finalize;

    directory_class->contains_file = merged_contains_file;
    directory_class->call_when_ready = merged_call_when_ready;
    directory_class->cancel_callback = merged_cancel_callback;
    directory_class->file_monitor_add = merged_monitor_add;
    directory_class->file_monitor_remove = merged_monitor_remove;
    directory_class->force_reload = merged_force_reload;
    directory_class->are_all_files_seen = merged_are_all_files_seen;
    directory_class->is_not_empty = merged_is_not_empty;
    /* Override get_file_list so that we can return a list that includes
     * the files from each of the directories in CajaMergedDirectory->details->directories.
         */
    directory_class->get_file_list = merged_get_file_list;

    class->add_real_directory = merged_add_real_directory;
    class->remove_real_directory = merged_remove_real_directory;

    signals[ADD_REAL_DIRECTORY]
        = g_signal_new ("add_real_directory",
                        G_TYPE_FROM_CLASS (class),
                        G_SIGNAL_RUN_LAST,
                        G_STRUCT_OFFSET (CajaMergedDirectoryClass,
                                         add_real_directory),
                        NULL, NULL,
                        g_cclosure_marshal_VOID__POINTER,
                        G_TYPE_NONE, 1, G_TYPE_POINTER);
    signals[REMOVE_REAL_DIRECTORY]
        = g_signal_new ("remove_real_directory",
                        G_TYPE_FROM_CLASS (class),
                        G_SIGNAL_RUN_LAST,
                        G_STRUCT_OFFSET (CajaMergedDirectoryClass,
                                         remove_real_directory),
                        NULL, NULL,
                        g_cclosure_marshal_VOID__POINTER,
                        G_TYPE_NONE, 1, G_TYPE_POINTER);
}