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

/* CajaUndoStackManager - Manages undo of file operations (implementation)
 *
 * Copyright (C) 2007-2010 Amos Brocco
 * Copyright (C) 2011 Stefano Karapetsas
 *
 * Authors: Amos Brocco <amos.brocco@unifr.ch>,
 *          Stefano Karapetsas <stefano@karapetsas.com>
 *
 * This library 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 library 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 library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "caja-undostack-manager.h"
#include "caja-file-operations.h"
#include "caja-file.h"
#include <gio/gio.h>
#include <glib/gprintf.h>
#include <glib-object.h>
#include <glib/gi18n.h>
#include <locale.h>
#include <gdk/gdk.h>

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

/* *****************************************************************
 Private fields
 ***************************************************************** */

struct _CajaUndoStackActionData
{
  /* Common stuff */
  CajaUndoStackActionType type;
  gboolean isValid;
  gboolean locked;              /* True if the action is being undone/redone */
  gboolean freed;               /* True if the action must be freed after undo/redo */
  guint count;                  /* Size of affected uris (count of items) */
  CajaUndoStackManager *manager;    /* Pointer to the manager */

  /* Copy / Move stuff */
  GFile *src_dir;
  GFile *dest_dir;
  GList *sources;               /* Relative to src_dir */
  GList *destinations;          /* Relative to dest_dir */

  /* Cached labels/descriptions */
  char *undo_label;
  char *undo_description;
  char *redo_label;
  char *redo_description;

  /* Create new file/folder stuff/set permissions */
  char *template;
  char *target_uri;

  /* Rename stuff */
  char *old_uri;
  char *new_uri;

  /* Trash stuff */
  GHashTable *trashed;

  /* Recursive change permissions stuff */
  GHashTable *original_permissions;
  guint32 dir_mask;
  guint32 dir_permissions;
  guint32 file_mask;
  guint32 file_permissions;

  /* Single file change permissions stuff */
  guint32 current_permissions;
  guint32 new_permissions;

  /* Group */
  char *original_group_name_or_id;
  char *new_group_name_or_id;

  /* Owner */
  char *original_user_name_or_id;
  char *new_user_name_or_id;

};

struct _CajaUndoStackManagerPrivate
{
  /* Private fields */
  GQueue *stack;
  guint undo_levels;
  guint index;
#if GLIB_CHECK_VERSION(2, 32, 0)
  GMutex mutex;                /* Used to protect access to stack (because of async file ops) */
#else
  GMutex *mutex;                /* Used to protect access to stack (because of async file ops) */
#endif
  gboolean dispose_has_run;
  gboolean undo_redo_flag;
  gboolean confirm_delete;
};

#define CAJA_UNDOSTACK_MANAGER_GET_PRIVATE(o)  \
   (G_TYPE_INSTANCE_GET_PRIVATE ((o), TYPE_CAJA_UNDOSTACK_MANAGER, CajaUndoStackManagerPrivate))

/* *****************************************************************
 Properties management prototypes
 ***************************************************************** */
enum
{
  PROP_UNDOSTACK_MANAGER_0, PROP_UNDO_LEVELS, PROP_CONFIRM_DELETE
};

static void caja_undostack_manager_set_property (GObject * object,
    guint prop_id, const GValue * value, GParamSpec * pspec);

static void caja_undostack_manager_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec);

/* *****************************************************************
 Destructors prototypes
 ***************************************************************** */
static void caja_undostack_manager_finalize (GObject * object);

static void caja_undostack_manager_dispose (GObject * object);

/* *****************************************************************
 Type definition
 ***************************************************************** */
G_DEFINE_TYPE (CajaUndoStackManager, caja_undostack_manager,
    G_TYPE_OBJECT);

/* *****************************************************************
 Private methods prototypes
 ***************************************************************** */

static void stack_clear_n_oldest (GQueue * stack, guint n);

static void stack_fix_size (CajaUndoStackManagerPrivate * priv);

static gboolean can_undo (CajaUndoStackManagerPrivate * priv);

static gboolean can_redo (CajaUndoStackManagerPrivate * priv);

static void stack_push_action (CajaUndoStackManagerPrivate * priv,
    CajaUndoStackActionData * action);

static CajaUndoStackActionData
    * stack_scroll_left (CajaUndoStackManagerPrivate * priv);

static CajaUndoStackActionData
    * stack_scroll_right (CajaUndoStackManagerPrivate * priv);

static CajaUndoStackActionData
    * get_next_redo_action (CajaUndoStackManagerPrivate * priv);

static CajaUndoStackActionData
    * get_next_undo_action (CajaUndoStackManagerPrivate * priv);

static gchar *get_undo_label (CajaUndoStackActionData * action);

static gchar *get_undo_description (CajaUndoStackActionData * action);

static gchar *get_redo_label (CajaUndoStackActionData * action);

static gchar *get_redo_description (CajaUndoStackActionData * action);

static void do_menu_update (CajaUndoStackManager * manager);

static void free_undostack_action (gpointer data, gpointer user_data);

static void undostack_dispose_all (GQueue * queue);

static void undo_redo_done_transfer_callback (GHashTable * debuting_uris,
    gpointer data);

static void undo_redo_op_callback (gpointer callback_data);

static void undo_redo_done_rename_callback (CajaFile * file,
    GFile * result_location, GError * error, gpointer callback_data);

static void undo_redo_done_delete_callback (GHashTable * debuting_uris,
    gboolean user_cancel, gpointer callback_data);

static void undo_redo_done_create_callback (GFile * new_file,
    gpointer callback_data);

static void clear_redo_actions (CajaUndoStackManagerPrivate * priv);

static gchar *get_first_target_short_name (CajaUndoStackActionData *
    action);

static GList *construct_gfile_list (const GList * urilist, GFile * parent);

static GList *construct_gfile_list_from_uri (char *uri);

static GList *uri_list_to_gfile_list (GList * urilist);

static char *get_uri_basename (char *uri);

static char *get_uri_parent (char *uri);

static char *get_uri_parent_path (char *uri);

static GHashTable *retrieve_files_to_restore (GHashTable * trashed);

/* *****************************************************************
 Base functions
 ***************************************************************** */
static void
caja_undostack_manager_class_init (CajaUndoStackManagerClass * klass)
{
  GParamSpec *undo_levels;
  GParamSpec *confirm_delete;
  GObjectClass *g_object_class;

  /* Add private structure */
  g_type_class_add_private (klass, sizeof (CajaUndoStackManagerPrivate));

  /* Create properties */
  undo_levels = g_param_spec_uint ("undo-levels", "undo levels",
      "Number of undo levels to be stored",
      1, UINT_MAX, 30, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);

  confirm_delete =
      g_param_spec_boolean ("confirm-delete", "confirm delete",
      "Always confirm file deletion", FALSE,
      G_PARAM_READWRITE | G_PARAM_CONSTRUCT);

  /* Set properties get/set methods */
  g_object_class = G_OBJECT_CLASS (klass);

  g_object_class->set_property = caja_undostack_manager_set_property;
  g_object_class->get_property = caja_undostack_manager_get_property;

  /* Install properties */
  g_object_class_install_property (g_object_class, PROP_UNDO_LEVELS,
      undo_levels);

  g_object_class_install_property (g_object_class, PROP_CONFIRM_DELETE,
      confirm_delete);

  /* The UI menu needs to update its status */
  g_signal_new ("request-menu-update",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE |
      G_SIGNAL_NO_HOOKS, 0, NULL, NULL,
      g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER);

  /* Hook deconstructors */
  g_object_class->dispose = caja_undostack_manager_dispose;
  g_object_class->finalize = caja_undostack_manager_finalize;
}

static void
caja_undostack_manager_init (CajaUndoStackManager * self)
{
  CajaUndoStackManagerPrivate *priv;

  priv = CAJA_UNDOSTACK_MANAGER_GET_PRIVATE (self);

  self->priv = priv;

  /* Initialize private fields */
  priv->stack = g_queue_new ();
#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_init (&priv->mutex);
#else
  priv->mutex = g_mutex_new ();
#endif
  priv->index = 0;
  priv->dispose_has_run = FALSE;
  priv->undo_redo_flag = FALSE;
  priv->confirm_delete = FALSE;
}

static void
caja_undostack_manager_dispose (GObject * object)
{
  CajaUndoStackManager *self = CAJA_UNDOSTACK_MANAGER (object);
  CajaUndoStackManagerPrivate *priv = self->priv;

  if (priv->dispose_has_run)
    return;

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif

  /* Free each undoable action in the stack and the stack itself */
  undostack_dispose_all (priv->stack);
  g_queue_free (priv->stack);
#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
  g_mutex_clear (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
  g_mutex_free (priv->mutex);
#endif

  priv->dispose_has_run = TRUE;

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

static void
caja_undostack_manager_finalize (GObject * object)
{
  G_OBJECT_CLASS (caja_undostack_manager_parent_class)->finalize (object);
}

/* *****************************************************************
 Property management
 ***************************************************************** */
static void
caja_undostack_manager_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  g_return_if_fail (IS_CAJA_UNDOSTACK_MANAGER (object));

  CajaUndoStackManager *manager = CAJA_UNDOSTACK_MANAGER (object);
  CajaUndoStackManagerPrivate *priv = manager->priv;
  guint new_undo_levels;

  switch (prop_id) {
    case PROP_UNDO_LEVELS:
      new_undo_levels = g_value_get_uint (value);
      if (new_undo_levels > 0 && (priv->undo_levels != new_undo_levels)) {
        priv->undo_levels = new_undo_levels;
#if GLIB_CHECK_VERSION(2, 32, 0)
        g_mutex_lock (&priv->mutex);
#else
        g_mutex_lock (priv->mutex);
#endif
        stack_fix_size (priv);
#if GLIB_CHECK_VERSION(2, 32, 0)
        g_mutex_unlock (&priv->mutex);
#else
        g_mutex_unlock (priv->mutex);
#endif
        do_menu_update (manager);
      }
      break;
    case PROP_CONFIRM_DELETE:
      priv->confirm_delete = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
caja_undostack_manager_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  g_return_if_fail (IS_CAJA_UNDOSTACK_MANAGER (object));

  CajaUndoStackManager *manager = CAJA_UNDOSTACK_MANAGER (object);
  CajaUndoStackManagerPrivate *priv = manager->priv;

  switch (prop_id) {
    case PROP_UNDO_LEVELS:
      g_value_set_uint (value, priv->undo_levels);
      break;

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

/* *****************************************************************
 Public methods
 ***************************************************************** */

/** ****************************************************************
 * Returns the undo stack manager instance (singleton pattern)
 ** ****************************************************************/
CajaUndoStackManager *
caja_undostack_manager_instance (void)
{
  static CajaUndoStackManager *manager = NULL;

  if (manager == NULL) {
    manager =
        g_object_new (TYPE_CAJA_UNDOSTACK_MANAGER, "undo-levels", 32, NULL);
  }

  return manager;
}

/** ****************************************************************
 * True if undoing / redoing
 ** ****************************************************************/
gboolean
caja_undostack_manager_is_undo_redo (CajaUndoStackManager * manager)
{
  CajaUndoStackManagerPrivate *priv = manager->priv;
  if (priv->undo_redo_flag) {
    return TRUE;
  }

  return FALSE;
}

void
caja_undostack_manager_request_menu_update (CajaUndoStackManager *
    manager)
{
  do_menu_update (manager);
}

/** ****************************************************************
 * Redoes the last file operation
 ** ****************************************************************/
void
caja_undostack_manager_redo (CajaUndoStackManager * manager,
    GtkWidget * parent_view, CajaUndostackFinishCallback cb)
{
  GList *uris;
  CajaFile *file;
  char *new_name;
  char *puri;
  CajaUndoStackManagerPrivate *priv = manager->priv;

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif


  CajaUndoStackActionData *action = stack_scroll_left (priv);

  /* Action will be NULL if redo is not possible */
  if (action != NULL) {
    action->locked = TRUE;
  }

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
#endif


  do_menu_update (manager);

  if (action != NULL) {
    action->locked = TRUE;      /* Remember to unlock when redo is finished */
    priv->undo_redo_flag = TRUE;
    switch (action->type) {
      case CAJA_UNDOSTACK_COPY:
        uris = construct_gfile_list (action->sources, action->src_dir);
        caja_file_operations_copy (uris, NULL,
            action->dest_dir, NULL, undo_redo_done_transfer_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
        puri = get_uri_parent (action->target_uri);
        new_name = get_uri_basename (action->target_uri);
        caja_file_operations_new_file_from_template (NULL,
            NULL,
            puri,
            new_name, action->template, undo_redo_done_create_callback, action);
        g_free (puri);
        g_free (new_name);
        break;
      case CAJA_UNDOSTACK_DUPLICATE:
        uris = construct_gfile_list (action->sources, action->src_dir);
        caja_file_operations_duplicate (uris, NULL, NULL,
            undo_redo_done_transfer_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_RESTOREFROMTRASH:
      case CAJA_UNDOSTACK_MOVE:
        uris = construct_gfile_list (action->sources, action->src_dir);
        caja_file_operations_move (uris, NULL,
            action->dest_dir, NULL, undo_redo_done_transfer_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_RENAME:
        new_name = get_uri_basename (action->new_uri);
        file = caja_file_get_by_uri (action->old_uri);
        caja_file_rename (file, new_name,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        g_free (new_name);
        break;
      case CAJA_UNDOSTACK_CREATEEMPTYFILE:
        puri = get_uri_parent (action->target_uri);
        new_name = get_uri_basename (action->target_uri);
        caja_file_operations_new_file (NULL, NULL, puri,
            new_name,
            action->template,
            0, undo_redo_done_create_callback, action);
        g_free (puri);
        g_free (new_name);
        break;
      case CAJA_UNDOSTACK_CREATEFOLDER:
        puri = get_uri_parent (action->target_uri);
        caja_file_operations_new_folder (NULL, NULL, puri,
            undo_redo_done_create_callback, action);
        g_free (puri);
        break;
      case CAJA_UNDOSTACK_MOVETOTRASH:
        if (g_hash_table_size (action->trashed) > 0) {
          GList *uri_to_trash = g_hash_table_get_keys (action->trashed);
          uris = uri_list_to_gfile_list (uri_to_trash);
          priv->undo_redo_flag = TRUE;
          caja_file_operations_trash_or_delete
              (uris, NULL, undo_redo_done_delete_callback, action);
          g_list_free (uri_to_trash);
    	  g_list_free_full (uris, g_object_unref);
        }
        break;
      case CAJA_UNDOSTACK_CREATELINK:
        uris = construct_gfile_list (action->sources, action->src_dir);
        caja_file_operations_link (uris, NULL,
            action->dest_dir, NULL, undo_redo_done_transfer_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_SETPERMISSIONS:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_permissions (file,
            action->new_permissions, undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
        puri = g_file_get_uri (action->dest_dir);
        caja_file_set_permissions_recursive (puri,
            action->file_permissions,
            action->file_mask,
            action->dir_permissions,
            action->dir_mask, undo_redo_op_callback, action);
        g_free (puri);
        break;
      case CAJA_UNDOSTACK_CHANGEGROUP:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_group (file,
            action->new_group_name_or_id,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_CHANGEOWNER:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_owner (file,
            action->new_user_name_or_id,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_DELETE:
      default:
        priv->undo_redo_flag = FALSE;
        break;                  /* We shouldn't be here */
    }
  }

  (*cb) ((gpointer) parent_view);
}

/** ****************************************************************
 * Undoes the last file operation
 ** ****************************************************************/
void
caja_undostack_manager_undo (CajaUndoStackManager * manager,
    GtkWidget * parent_view, CajaUndostackFinishCallback cb)
{
  GList *uris = NULL;
  GHashTable *files_to_restore;
  CajaFile *file;
  char *new_name;
  CajaUndoStackManagerPrivate *priv = manager->priv;

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif

  CajaUndoStackActionData *action = stack_scroll_right (priv);

  if (action != NULL) {
    action->locked = TRUE;
  }

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
#endif

  do_menu_update (manager);

  if (action != NULL) {
    priv->undo_redo_flag = TRUE;
    switch (action->type) {
      case CAJA_UNDOSTACK_CREATEEMPTYFILE:
      case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
      case CAJA_UNDOSTACK_CREATEFOLDER:
        uris = construct_gfile_list_from_uri (action->target_uri);
      case CAJA_UNDOSTACK_COPY:
      case CAJA_UNDOSTACK_DUPLICATE:
      case CAJA_UNDOSTACK_CREATELINK:
        if (!uris) {
          uris = construct_gfile_list (action->destinations, action->dest_dir);
          uris = g_list_reverse (uris); // Deleting must be done in reverse
        }
        if (priv->confirm_delete) {
          caja_file_operations_delete (uris, NULL,
              undo_redo_done_delete_callback, action);
    	  g_list_free_full (uris, g_object_unref);
        } else {
          /* We skip the confirmation message
           */
          GList *f;
          for (f = uris; f != NULL; f = f->next) {
            char *name;
            name = g_file_get_uri (f->data);
            g_free (name);
            g_file_delete (f->data, NULL, NULL);
            g_object_unref (f->data);
          }
          g_list_free (uris);
          /* Here we must do what's necessary for the callback */
          undo_redo_done_transfer_callback (NULL, action);
        }
        break;
      case CAJA_UNDOSTACK_RESTOREFROMTRASH:
        uris = construct_gfile_list (action->destinations, action->dest_dir);
        caja_file_operations_trash_or_delete (uris, NULL,
            undo_redo_done_delete_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_MOVETOTRASH:
        files_to_restore = retrieve_files_to_restore (action->trashed);
        if (g_hash_table_size (files_to_restore) > 0) {
          GList *l;
          GList *gfiles_in_trash = g_hash_table_get_keys (files_to_restore);
          GFile *item;
          GFile *dest;
          char *value;

          for (l = gfiles_in_trash; l != NULL; l = l->next) {
            item = l->data;
            value = g_hash_table_lookup (files_to_restore, item);
            dest = g_file_new_for_uri (value);
            g_file_move (item, dest,
                G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL, NULL, NULL);
            g_object_unref (dest);
          }

          g_list_free (gfiles_in_trash);
        }
        g_hash_table_destroy (files_to_restore);
        /* Here we must do what's necessary for the callback */
        undo_redo_done_transfer_callback (NULL, action);
        break;
      case CAJA_UNDOSTACK_MOVE:
        uris = construct_gfile_list (action->destinations, action->dest_dir);
        caja_file_operations_move (uris, NULL,
            action->src_dir, NULL, undo_redo_done_transfer_callback, action);
    	g_list_free_full (uris, g_object_unref);
        break;
      case CAJA_UNDOSTACK_RENAME:
        new_name = get_uri_basename (action->old_uri);
        file = caja_file_get_by_uri (action->new_uri);
        caja_file_rename (file, new_name,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        g_free (new_name);
        break;
      case CAJA_UNDOSTACK_SETPERMISSIONS:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_permissions (file,
            action->current_permissions,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
        if (g_hash_table_size (action->original_permissions) > 0) {
          GList *gfiles_list =
              g_hash_table_get_keys (action->original_permissions);
          guint32 *perm;
          GList *l;
          GFile *dest;
          char *item;

          for (l = gfiles_list; l != NULL; l = l->next) {
            item = l->data;
            perm = g_hash_table_lookup (action->original_permissions, item);
            dest = g_file_new_for_uri (item);
            g_file_set_attribute_uint32 (dest,
                G_FILE_ATTRIBUTE_UNIX_MODE,
                *perm, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
            g_object_unref (dest);
          }

          g_list_free (gfiles_list);
          /* Here we must do what's necessary for the callback */
          undo_redo_done_transfer_callback (NULL, action);
        }
        break;
      case CAJA_UNDOSTACK_CHANGEGROUP:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_group (file,
            action->original_group_name_or_id,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_CHANGEOWNER:
        file = caja_file_get_by_uri (action->target_uri);
        caja_file_set_owner (file,
            action->original_user_name_or_id,
            undo_redo_done_rename_callback, action);
        g_object_unref (file);
        break;
      case CAJA_UNDOSTACK_DELETE:
      default:
        priv->undo_redo_flag = FALSE;
        break;                  /* We shouldn't be here */
    }
  }

  (*cb) ((gpointer) parent_view);
}

/** ****************************************************************
 * Adds an operation to the stack
 ** ****************************************************************/
void
caja_undostack_manager_add_action (CajaUndoStackManager * manager,
    CajaUndoStackActionData * action)
{
  CajaUndoStackManagerPrivate *priv = manager->priv;

  if (!action)
    return;

  if (!(action && action->isValid)) {
    free_undostack_action ((gpointer) action, NULL);
    return;
  }

  action->manager = manager;

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif

  stack_push_action (priv, action);

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
#endif

  do_menu_update (manager);

}

static GList *
get_all_trashed_items (GQueue *stack)
{
  CajaUndoStackActionData *action = NULL;
  GList *trash = NULL;
  GList *l;
  GQueue *tmp_stack = g_queue_copy(stack);

  while ((action = (CajaUndoStackActionData *) g_queue_pop_tail (tmp_stack)) != NULL)
    if (action->trashed)
        for (l = g_hash_table_get_keys (action->trashed); l != NULL; l=l->next) { 
                trash = g_list_append(trash, l->data);
        }
  
  g_queue_free (tmp_stack);
  return (trash);
}

static gboolean 
is_destination_uri_action_partof_trashed(GList *trash, GList *g)
{
    GList *l;
    char *uri;

    for (l = trash; l != NULL; l=l->next) { 
        for (; g != NULL; g=g->next) { 
            //printf ("destinations: %s\n", g_file_get_uri(l->data));
            uri = g_file_get_uri(g->data);
            if (!strcmp (uri, l->data)) {
               //printf ("GG %s\nZZ %s\n", uri, l->data);
               g_free (uri);
               return TRUE;
            }
            g_free (uri);
        }
    }

    return FALSE;
}
/** ****************************************************************
 * Callback after emptying the trash
 ** ****************************************************************/
void
caja_undostack_manager_trash_has_emptied (CajaUndoStackManager *
    manager)
{
  CajaUndoStackManagerPrivate *priv = manager->priv;

  /* Clear actions from the oldest to the newest move to trash */

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif

  clear_redo_actions (priv);
  CajaUndoStackActionData *action = NULL;

  GList *g;
  GQueue *tmp_stack = g_queue_copy(priv->stack);
  GList *trash = get_all_trashed_items (tmp_stack);
  while ((action = (CajaUndoStackActionData *) g_queue_pop_tail (tmp_stack)) != NULL)
  {
    if (action->destinations && action->dest_dir) {
        /* what a pain rebuild again and again an uri
        ** TODO change the struct add uri elements */
        g = construct_gfile_list (action->destinations, action->dest_dir);
        /* remove action for trashed item uris == destination action */
        if (is_destination_uri_action_partof_trashed(trash, g)) {
                g_queue_remove (priv->stack, action);
                continue;
        }
    }
    if (action->type == CAJA_UNDOSTACK_MOVETOTRASH) {
        g_queue_remove (priv->stack, action);
    }
  }

  g_queue_free (tmp_stack);
#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
#endif
  do_menu_update (manager);
}

/** ****************************************************************
 * Returns the modification time for the given file (used for undo trash)
 ** ****************************************************************/
guint64
caja_undostack_manager_get_file_modification_time (GFile * file)
{
  GFileInfo *info;
  guint64 mtime;

  info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED,
      G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, FALSE, NULL);
  if (info == NULL) {
    return -1;
  }

  mtime = g_file_info_get_attribute_uint64 (info,
      G_FILE_ATTRIBUTE_TIME_MODIFIED);

  g_object_unref (info);

  return mtime;
}

/** ****************************************************************
 * Returns a new undo data container
 ** ****************************************************************/
CajaUndoStackActionData *
caja_undostack_manager_data_new (CajaUndoStackActionType type,
    gint items_count)
{
  CajaUndoStackActionData *data =
      g_slice_new0 (CajaUndoStackActionData);
  data->type = type;
  data->count = items_count;

  if (type == CAJA_UNDOSTACK_MOVETOTRASH) {
    data->trashed =
        g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  } else if (type == CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS) {
    data->original_permissions =
        g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  }

  return data;
}

/** ****************************************************************
 * Sets the source directory
 ** ****************************************************************/
void
caja_undostack_manager_data_set_src_dir (CajaUndoStackActionData *
    data, GFile * src)
{
  if (!data)
    return;

  data->src_dir = src;
}

/** ****************************************************************
 * Sets the destination directory
 ** ****************************************************************/
void
caja_undostack_manager_data_set_dest_dir (CajaUndoStackActionData *
    data, GFile * dest)
{
  if (!data)
    return;

  data->dest_dir = dest;
}

/** ****************************************************************
 * Pushes an origin, target pair in an existing undo data container
 ** ****************************************************************/
void caja_undostack_manager_data_add_origin_target_pair
    (CajaUndoStackActionData * data, GFile * origin, GFile * target)
{

  if (!data)
    return;

  char *src_relative = g_file_get_relative_path (data->src_dir, origin);
  data->sources = g_list_append (data->sources, src_relative);
  char *dest_relative = g_file_get_relative_path (data->dest_dir, target);
  data->destinations = g_list_append (data->destinations, dest_relative);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Pushes an trashed file with modification time in an existing undo data container
 ** ****************************************************************/
void
caja_undostack_manager_data_add_trashed_file (CajaUndoStackActionData
    * data, GFile * file, guint64 mtime)
{

  if (!data)
    return;

  guint64 *modificationTime;
  modificationTime = (guint64 *) malloc (sizeof (guint64));
  *modificationTime = mtime;

  char *originalURI = g_file_get_uri (file);

  g_hash_table_insert (data->trashed, originalURI, modificationTime);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Pushes a recursive permission change data in an existing undo data container
 ** ****************************************************************/
void caja_undostack_manager_data_add_file_permissions
    (CajaUndoStackActionData * data, GFile * file, guint32 permission)
{

  if (!data)
    return;

  guint32 *currentPermission;
  currentPermission = (guint32 *) malloc (sizeof (guint32));
  *currentPermission = permission;

  char *originalURI = g_file_get_uri (file);

  g_hash_table_insert (data->original_permissions, originalURI,
      currentPermission);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets the original file permission in an existing undo data container
 ** ****************************************************************/
void caja_undostack_manager_data_set_file_permissions
    (CajaUndoStackActionData * data, char *uri,
    guint32 current_permissions, guint32 new_permissions)
{

  if (!data)
    return;

  data->target_uri = uri;

  data->current_permissions = current_permissions;
  data->new_permissions = new_permissions;

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets the change owner information in an existing undo data container
 ** ****************************************************************/
void caja_undostack_manager_data_set_owner_change_information
    (CajaUndoStackActionData * data, char *uri,
    const char *current_user, const char *new_user)
{

  if (!data)
    return;

  data->target_uri = uri;

  data->original_user_name_or_id = g_strdup (current_user);
  data->new_user_name_or_id = g_strdup (new_user);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets the change group information in an existing undo data container
 ** ****************************************************************/
void caja_undostack_manager_data_set_group_change_information
    (CajaUndoStackActionData * data, char *uri,
    const char *current_group, const char *new_group)
{

  if (!data)
    return;

  data->target_uri = uri;

  data->original_group_name_or_id = g_strdup (current_group);
  data->new_group_name_or_id = g_strdup (new_group);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets the permission change mask
 ** ****************************************************************/
void caja_undostack_manager_data_set_recursive_permissions
    (CajaUndoStackActionData * data, guint32 file_permissions,
    guint32 file_mask, guint32 dir_permissions, guint32 dir_mask)
{

  if (!data)
    return;

  data->file_permissions = file_permissions;
  data->file_mask = file_mask;
  data->dir_permissions = dir_permissions;
  data->dir_mask = dir_mask;

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets create file information
 ** ****************************************************************/
void
caja_undostack_manager_data_set_create_data (CajaUndoStackActionData *
    data, char *target_uri, char *template)
{

  if (!data)
    return;

  data->template = g_strdup (template);
  data->target_uri = g_strdup (target_uri);

  data->isValid = TRUE;
}

/** ****************************************************************
 * Sets rename information
 ** ****************************************************************/
void caja_undostack_manager_data_set_rename_information
    (CajaUndoStackActionData * data, GFile * old_file, GFile * new_file)
{

  if (!data)
    return;

  data->old_uri = g_file_get_uri (old_file);
  data->new_uri = g_file_get_uri (new_file);

  data->isValid = TRUE;
}

/* *****************************************************************
 Private methods (nothing to see here, move along)
 ***************************************************************** */

static CajaUndoStackActionData *
stack_scroll_right (CajaUndoStackManagerPrivate * priv)
{
  gpointer data = NULL;

  if (!can_undo (priv))
    return NULL;

  data = g_queue_peek_nth (priv->stack, priv->index);
  if (priv->index < g_queue_get_length (priv->stack)) {
    priv->index++;
  }

  return data;
}

/** ---------------------------------------------------------------- */
static CajaUndoStackActionData *
stack_scroll_left (CajaUndoStackManagerPrivate * priv)
{
  gpointer data = NULL;

  if (!can_redo (priv))
    return NULL;

  priv->index--;
  data = g_queue_peek_nth (priv->stack, priv->index);

  return data;
}

/** ---------------------------------------------------------------- */
static void
stack_clear_n_oldest (GQueue * stack, guint n)
{
  CajaUndoStackActionData *action;
  guint i;

  for (i = 0; i < n; i++) {
    if ((action = (CajaUndoStackActionData *) g_queue_pop_tail (stack)) == NULL)
        break;
    if (action->locked) {
      action->freed = TRUE;
    } else {
      free_undostack_action (action, NULL);
    }
  }
}

/** ---------------------------------------------------------------- */
static void
stack_fix_size (CajaUndoStackManagerPrivate * priv)
{
  guint length = g_queue_get_length (priv->stack);

  if (length > priv->undo_levels) {
    if (priv->index > (priv->undo_levels + 1)) {
      /* If the index will fall off the stack
       * move it back to the maximum position */
      priv->index = priv->undo_levels + 1;
    }
    stack_clear_n_oldest (priv->stack, length - (priv->undo_levels));
  }
}

/** ---------------------------------------------------------------- */
static void
clear_redo_actions (CajaUndoStackManagerPrivate * priv)
{
  while (priv->index > 0) {
    CajaUndoStackActionData *head = (CajaUndoStackActionData *)
        g_queue_pop_head (priv->stack);
    free_undostack_action (head, NULL);
    priv->index--;
  }
}

/** ---------------------------------------------------------------- */
static void
stack_push_action (CajaUndoStackManagerPrivate * priv,
    CajaUndoStackActionData * action)
{
  guint length;

  clear_redo_actions (priv);

  g_queue_push_head (priv->stack, (gpointer) action);
  length = g_queue_get_length (priv->stack);

  if (length > priv->undo_levels) {
    stack_fix_size (priv);
  }
}

/** ---------------------------------------------------------------- */
static gchar *
get_first_target_short_name (CajaUndoStackActionData * action)
{
  GList *targets_first;
  gchar *file_name;

  targets_first = g_list_first (action->destinations);
  file_name = (gchar *) g_strdup (targets_first->data);

  return file_name;
}

/** ---------------------------------------------------------------- */
static gchar *
get_undo_description (CajaUndoStackActionData * action)
{
  gchar *description = NULL;
  gchar *source = NULL;
  guint count;

  if (action != NULL) {
    if (action->undo_description == NULL) {
      if (action->src_dir) {
        source = g_file_get_path (action->src_dir);
      }
      count = action->count;
      switch (action->type) {
        case CAJA_UNDOSTACK_COPY:
          if (count != 1) {
            description = g_strdup_printf (_("Delete %d copied items"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Delete '%s'"), name);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_DUPLICATE:
          if (count != 1) {
            description =
                g_strdup_printf (_("Delete %d duplicated items"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Delete '%s'"), name);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_MOVE:
          if (count != 1) {
            description =
                g_strdup_printf (_
                ("Move %d items back to '%s'"), count, source);
          } else {
            gchar *name = get_first_target_short_name (action);
            description =
                g_strdup_printf (_("Move '%s' back to '%s'"), name, source);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_RENAME:
        {
          char *from_name = get_uri_basename (action->new_uri);
          char *to_name = get_uri_basename (action->old_uri);
          description =
              g_strdup_printf (_("Rename '%s' as '%s'"), from_name, to_name);
          g_free (from_name);
          g_free (to_name);
        }
          break;
        case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
        case CAJA_UNDOSTACK_CREATEEMPTYFILE:
        case CAJA_UNDOSTACK_CREATEFOLDER:
        {
          char *name = get_uri_basename (action->target_uri);
          description = g_strdup_printf (_("Delete '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_MOVETOTRASH:
        {
          count = g_hash_table_size (action->trashed);
          if (count != 1) {
            description =
                g_strdup_printf (_("Restore %d items from trash"), count);
          } else {
            GList *keys = g_hash_table_get_keys (action->trashed);
            GList *first = g_list_first (keys);
            char *item = (char *) first->data;
            char *name = get_uri_basename (item);
            char *orig_path = get_uri_parent_path (item);
            description =
                g_strdup_printf (_("Restore '%s' to '%s'"), name, orig_path);
            g_free (name);
            g_free (orig_path);
            g_list_free (keys);
          }
        }
          break;
        case CAJA_UNDOSTACK_RESTOREFROMTRASH:
        {
          if (count != 1) {
            description =
                g_strdup_printf (_("Move %d items back to trash"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Move '%s' back to trash"), name);
            g_free (name);
          }
        }
          break;
        case CAJA_UNDOSTACK_CREATELINK:
        {
          if (count != 1) {
            description =
                g_strdup_printf (_("Delete links to %d items"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Delete link to '%s'"), name);
            g_free (name);
          }
        }
          break;
        case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
        {
          char *name = g_file_get_path (action->dest_dir);
          description =
              g_strdup_printf (_
              ("Restore original permissions of items enclosed in '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_SETPERMISSIONS:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_("Restore original permissions of '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CHANGEGROUP:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_
              ("Restore group of '%s' to '%s'"),
              name, action->original_group_name_or_id);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CHANGEOWNER:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_
              ("Restore owner of '%s' to '%s'"),
              name, action->original_user_name_or_id);
          g_free (name);
        }
          break;
        default:
          break;
      }
      if (source) {
        g_free (source);
      }
      action->undo_description = description;
    } else {
      return action->undo_description;
    }
  }

  return description;
}

/** ---------------------------------------------------------------- */
static gchar *
get_redo_description (CajaUndoStackActionData * action)
{
  gchar *description = NULL;
  gchar *destination = NULL;
  guint count;

  if (action != NULL) {
    if (action->redo_description == NULL) {
      if (action->dest_dir) {
        destination = g_file_get_path (action->dest_dir);
      }
      count = action->count;
      switch (action->type) {
        case CAJA_UNDOSTACK_COPY:
          if (count != 1) {
            description =
                g_strdup_printf (_
                ("Copy %d items to '%s'"), count, destination);
          } else {
            gchar *name = get_first_target_short_name (action);
            description =
                g_strdup_printf (_("Copy '%s' to '%s'"), name, destination);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_DUPLICATE:
          if (count != 1) {
            description =
                g_strdup_printf (_
                ("Duplicate of %d items in '%s'"), count, destination);
          } else {
            gchar *name = get_first_target_short_name (action);
            description =
                g_strdup_printf (_
                ("Duplicate '%s' in '%s'"), name, destination);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_MOVE:
          if (count != 1) {
            description =
                g_strdup_printf (_
                ("Move %d items to '%s'"), count, destination);
          } else {
            gchar *name = get_first_target_short_name (action);
            description =
                g_strdup_printf (_("Move '%s' to '%s'"), name, destination);
            g_free (name);
          }
          break;
        case CAJA_UNDOSTACK_RENAME:
        {
          char *from_name = get_uri_basename (action->old_uri);
          char *to_name = get_uri_basename (action->new_uri);
          description =
              g_strdup_printf (_("Rename '%s' as '%s'"), from_name, to_name);
          g_free (from_name);
          g_free (to_name);
        }
          break;
        case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_("Create new file '%s' from template "), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CREATEEMPTYFILE:
        {
          char *name = get_uri_basename (action->target_uri);
          description = g_strdup_printf (_("Create an empty file '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CREATEFOLDER:
        {
          char *name = get_uri_basename (action->target_uri);
          description = g_strdup_printf (_("Create a new folder '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_MOVETOTRASH:
        {
          count = g_hash_table_size (action->trashed);
          if (count != 1) {
            description = g_strdup_printf (_("Move %d items to trash"), count);
          } else {
            GList *keys = g_hash_table_get_keys (action->trashed);
            GList *first = g_list_first (keys);
            char *item = (char *) first->data;
            char *name = get_uri_basename (item);
            description = g_strdup_printf (_("Move '%s' to trash"), name);
            g_free (name);
            g_list_free (keys);
          }
        }
          break;
        case CAJA_UNDOSTACK_RESTOREFROMTRASH:
        {
          if (count != 1) {
            description =
                g_strdup_printf (_("Restore %d items from trash"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Restore '%s' from trash"), name);
            g_free (name);
          }
        }
          break;
        case CAJA_UNDOSTACK_CREATELINK:
        {
          if (count != 1) {
            description =
                g_strdup_printf (_("Create links to %d items"), count);
          } else {
            gchar *name = get_first_target_short_name (action);
            description = g_strdup_printf (_("Create link to '%s'"), name);
            g_free (name);
          }
        }
          break;
        case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
        {
          char *name = g_file_get_path (action->dest_dir);
          description =
              g_strdup_printf (_("Set permissions of items enclosed in '%s'"),
              name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_SETPERMISSIONS:
        {
          char *name = get_uri_basename (action->target_uri);
          description = g_strdup_printf (_("Set permissions of '%s'"), name);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CHANGEGROUP:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_
              ("Set group of '%s' to '%s'"),
              name, action->new_group_name_or_id);
          g_free (name);
        }
          break;
        case CAJA_UNDOSTACK_CHANGEOWNER:
        {
          char *name = get_uri_basename (action->target_uri);
          description =
              g_strdup_printf (_
              ("Set owner of '%s' to '%s'"), name, action->new_user_name_or_id);
          g_free (name);
        }
          break;
        default:
          break;
      }
      if (destination) {
        g_free (destination);
      }
      action->redo_description = description;
    } else {
      return action->redo_description;
    }
  }

  return description;
}

/** ---------------------------------------------------------------- */
static gchar *
get_undo_label (CajaUndoStackActionData * action)
{
  gchar *label = NULL;
  guint count;

  if (action != NULL) {
    if (action->undo_label == NULL) {
      count = action->count;
      switch (action->type) {
        case CAJA_UNDOSTACK_COPY:
          label = g_strdup_printf (ngettext
              ("_Undo copy of %d item",
                  "_Undo copy of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_DUPLICATE:
          label = g_strdup_printf (ngettext
              ("_Undo duplicate of %d item",
                  "_Undo duplicate of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_MOVE:
          label = g_strdup_printf (ngettext
              ("_Undo move of %d item",
                  "_Undo move of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RENAME:
          label = g_strdup_printf (ngettext
              ("_Undo rename of %d item",
                  "_Undo rename of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CREATEEMPTYFILE:
          label = g_strdup_printf (_("_Undo creation of an empty file"));
          break;
        case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
          label = g_strdup_printf (_("_Undo creation of a file from template"));
          break;
        case CAJA_UNDOSTACK_CREATEFOLDER:
          label = g_strdup_printf (ngettext
              ("_Undo creation of %d folder",
                  "_Undo creation of %d folders", count), count);
          break;
        case CAJA_UNDOSTACK_MOVETOTRASH:
          label = g_strdup_printf (ngettext
              ("_Undo move to trash of %d item",
                  "_Undo move to trash of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RESTOREFROMTRASH:
          label = g_strdup_printf (ngettext
              ("_Undo restore from trash of %d item",
                  "_Undo restore from trash of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CREATELINK:
          label = g_strdup_printf (ngettext
              ("_Undo create link to %d item",
                  "_Undo create link to %d items", count), count);
          break;
        case CAJA_UNDOSTACK_DELETE:
          label = g_strdup_printf (ngettext
              ("_Undo delete of %d item",
                  "_Undo delete of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
          label = g_strdup_printf (ngettext
              ("Undo recursive change permissions of %d item",
                  "Undo recursive change permissions of %d items",
                  count), count);
          break;
        case CAJA_UNDOSTACK_SETPERMISSIONS:
          label = g_strdup_printf (ngettext
              ("Undo change permissions of %d item",
                  "Undo change permissions of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CHANGEGROUP:
          label = g_strdup_printf (ngettext
              ("Undo change group of %d item",
                  "Undo change group of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CHANGEOWNER:
          label = g_strdup_printf (ngettext
              ("Undo change owner of %d item",
                  "Undo change owner of %d items", count), count);
          break;
        default:
          break;
      }
      action->undo_label = label;
    } else {
      return action->undo_label;
    }
  }

  return label;
}

/** ---------------------------------------------------------------- */
static gchar *
get_redo_label (CajaUndoStackActionData * action)
{
  gchar *label = NULL;
  guint count;

  if (action != NULL) {
    if (action->redo_label == NULL) {
      count = action->count;
      switch (action->type) {
        case CAJA_UNDOSTACK_COPY:
          label = g_strdup_printf (ngettext
              ("_Redo copy of %d item",
                  "_Redo copy of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_DUPLICATE:
          label = g_strdup_printf (ngettext
              ("_Redo duplicate of %d item",
                  "_Redo duplicate of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_MOVE:
          label = g_strdup_printf (ngettext
              ("_Redo move of %d item",
                  "_Redo move of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RENAME:
          label = g_strdup_printf (ngettext
              ("_Redo rename of %d item",
                  "_Redo rename of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CREATEEMPTYFILE:
          label = g_strdup_printf (_("_Redo creation of an empty file"));
          break;
        case CAJA_UNDOSTACK_CREATEFILEFROMTEMPLATE:
          label = g_strdup_printf (_("_Redo creation of a file from template"));
          break;
        case CAJA_UNDOSTACK_CREATEFOLDER:
          label = g_strdup_printf (ngettext
              ("_Redo creation of %d folder",
                  "_Redo creation of %d folders", count), count);
          break;
        case CAJA_UNDOSTACK_MOVETOTRASH:
          label = g_strdup_printf (ngettext
              ("_Redo move to trash of %d item",
                  "_Redo move to trash of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RESTOREFROMTRASH:
          label = g_strdup_printf (ngettext
              ("_Redo restore from trash of %d item",
                  "_Redo restore from trash of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CREATELINK:
          label = g_strdup_printf (ngettext
              ("_Redo create link to %d item",
                  "_Redo create link to %d items", count), count);
          break;
        case CAJA_UNDOSTACK_DELETE:
          label = g_strdup_printf (ngettext
              ("_Redo delete of %d item",
                  "_Redo delete of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_RECURSIVESETPERMISSIONS:
          label = g_strdup_printf (ngettext
              ("Redo recursive change permissions of %d item",
                  "Redo recursive change permissions of %d items",
                  count), count);
          break;
        case CAJA_UNDOSTACK_SETPERMISSIONS:
          label = g_strdup_printf (ngettext
              ("Redo change permissions of %d item",
                  "Redo change permissions of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CHANGEGROUP:
          label = g_strdup_printf (ngettext
              ("Redo change group of %d item",
                  "Redo change group of %d items", count), count);
          break;
        case CAJA_UNDOSTACK_CHANGEOWNER:
          label = g_strdup_printf (ngettext
              ("Redo change owner of %d item",
                  "Redo change owner of %d items", count), count);
          break;
        default:
          break;
      }
      action->redo_label = label;
    } else {
      return action->redo_label;
    }
  }

  return label;
}

/** ---------------------------------------------------------------- */
static void
undo_redo_done_transfer_callback (GHashTable * debuting_uris, gpointer data)
{
  CajaUndoStackActionData *action;

  action = (CajaUndoStackActionData *) data;

  /* If the action needed to be freed but was locked, free now */
  if (action->freed) {
    free_undostack_action (action, NULL);
  } else {
    action->locked = FALSE;
  }

  CajaUndoStackManager *manager = action->manager;
  manager->priv->undo_redo_flag = FALSE;

  /* Update menus */
  do_menu_update (action->manager);
}

/** ---------------------------------------------------------------- */
static void
undo_redo_done_delete_callback (GHashTable *
    debuting_uris, gboolean user_cancel, gpointer callback_data)
{
  undo_redo_done_transfer_callback (debuting_uris, callback_data);
}

/** ---------------------------------------------------------------- */
static void
undo_redo_done_create_callback (GFile * new_file, gpointer callback_data)
{
  undo_redo_done_transfer_callback (NULL, callback_data);
}

/** ---------------------------------------------------------------- */
static void
undo_redo_op_callback (gpointer callback_data)
{
  undo_redo_done_transfer_callback (NULL, callback_data);
}

/** ---------------------------------------------------------------- */
static void
undo_redo_done_rename_callback (CajaFile * file,
    GFile * result_location, GError * error, gpointer callback_data)
{
  undo_redo_done_transfer_callback (NULL, callback_data);
}

/** ---------------------------------------------------------------- */
static void
free_undostack_action (gpointer data, gpointer user_data)
{
  CajaUndoStackActionData *action = (CajaUndoStackActionData *) data;

  if (!action)
    return;

  g_free (action->template);
  g_free (action->target_uri);
  g_free (action->old_uri);
  g_free (action->new_uri);

  g_free (action->undo_label);
  g_free (action->undo_description);
  g_free (action->redo_label);
  g_free (action->redo_description);

  g_free (action->original_group_name_or_id);
  g_free (action->original_user_name_or_id);
  g_free (action->new_group_name_or_id);
  g_free (action->new_user_name_or_id);

  if (action->sources) {
    g_list_free_full (action->sources, g_free);
  }
  if (action->destinations) {
    g_list_free_full (action->destinations, g_free);
  }

  if (action->trashed) {
    g_hash_table_destroy (action->trashed);
  }

  if (action->original_permissions) {
    g_hash_table_destroy (action->original_permissions);
  }

  if (action->src_dir)
    g_object_unref (action->src_dir);
  if (action->dest_dir)
    g_object_unref (action->dest_dir);

  if (action)
    g_slice_free (CajaUndoStackActionData, action);
}

/** ---------------------------------------------------------------- */
static void
undostack_dispose_all (GQueue * queue)
{
  g_queue_foreach (queue, free_undostack_action, NULL);
}

/** ---------------------------------------------------------------- */
static gboolean
can_undo (CajaUndoStackManagerPrivate * priv)
{
  return (get_next_undo_action (priv) != NULL);
}

/** ---------------------------------------------------------------- */
static gboolean
can_redo (CajaUndoStackManagerPrivate * priv)
{
  return (get_next_redo_action (priv) != NULL);
}

/** ---------------------------------------------------------------- */
static CajaUndoStackActionData *
get_next_redo_action (CajaUndoStackManagerPrivate * priv)
{
  if (g_queue_is_empty (priv->stack)) {
    return NULL;
  }

  if (priv->index == 0) {
    /* ... no redo actions */
    return NULL;
  }

  CajaUndoStackActionData *action = g_queue_peek_nth (priv->stack,
      priv->index - 1);

  if (action->locked) {
    return NULL;
  } else {
    return action;
  }
}

/** ---------------------------------------------------------------- */
static CajaUndoStackActionData *
get_next_undo_action (CajaUndoStackManagerPrivate * priv)
{
  if (g_queue_is_empty (priv->stack)) {
    return NULL;
  }

  guint stack_size = g_queue_get_length (priv->stack);

  if (priv->index == stack_size) {
    return NULL;
  }

  CajaUndoStackActionData *action = g_queue_peek_nth (priv->stack,
      priv->index);

  if (action->locked) {
    return NULL;
  } else {
    return action;
  }
}

/** ---------------------------------------------------------------- */
static void
do_menu_update (CajaUndoStackManager * manager)
{

  if (!manager)
    return;

  CajaUndoStackActionData *action;
  CajaUndoStackManagerPrivate *priv = manager->priv;
  CajaUndoStackMenuData *data = g_slice_new0 (CajaUndoStackMenuData);

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_lock (&priv->mutex);
#else
  g_mutex_lock (priv->mutex);
#endif

  action = get_next_undo_action (priv);
  data->undo_label = get_undo_label (action);
  data->undo_description = get_undo_description (action);

  action = get_next_redo_action (priv);

  data->redo_label = get_redo_label (action);
  data->redo_description = get_redo_description (action);

#if GLIB_CHECK_VERSION(2, 32, 0)
  g_mutex_unlock (&priv->mutex);
#else
  g_mutex_unlock (priv->mutex);
#endif

  /* Update menus */
  g_signal_emit_by_name (manager, "request-menu-update", data);

  /* Free the signal data */
  // Note: we do not own labels and descriptions, they are part of the action.
  g_slice_free (CajaUndoStackMenuData, data);
}

/** ---------------------------------------------------------------- */
static GList *
construct_gfile_list (const GList * urilist, GFile * parent)
{
  const GList *l;
  GList *file_list = NULL;
  GFile *file;

  for (l = urilist; l != NULL; l = l->next) {
    file = g_file_get_child (parent, l->data);
    file_list = g_list_append (file_list, file);
  }

  return file_list;
}

/** ---------------------------------------------------------------- */
static GList *
construct_gfile_list_from_uri (char *uri)
{
  GList *file_list = NULL;
  GFile *file;

  file = g_file_new_for_uri (uri);
  file_list = g_list_append (file_list, file);

  return file_list;
}

/** ---------------------------------------------------------------- */
static GList *
uri_list_to_gfile_list (GList * urilist)
{
  const GList *l;
  GList *file_list = NULL;
  GFile *file;

  for (l = urilist; l != NULL; l = l->next) {
    file = g_file_new_for_uri (l->data);
    file_list = g_list_append (file_list, file);
  }

  return file_list;
}

/** ---------------------------------------------------------------- */
static char *
get_uri_basename (char *uri)
{
  GFile *f = g_file_new_for_uri (uri);
  char *basename = g_file_get_basename (f);
  g_object_unref (f);
  return basename;
}

/** ---------------------------------------------------------------- */
static char *
get_uri_parent (char *uri)
{
  GFile *f = g_file_new_for_uri (uri);
  GFile *p = g_file_get_parent (f);
  char *parent = g_file_get_uri (p);
  g_object_unref (f);
  g_object_unref (p);
  return parent;
}

/** ---------------------------------------------------------------- */
static char *
get_uri_parent_path (char *uri)
{
  GFile *f = g_file_new_for_uri (uri);
  GFile *p = g_file_get_parent (f);
  char *parent = g_file_get_path (p);
  g_object_unref (f);
  g_object_unref (p);
  return parent;
}

/** ---------------------------------------------------------------- */
static GHashTable *
retrieve_files_to_restore (GHashTable * trashed)
{
  if (!(g_hash_table_size (trashed)) > 0) {
    return NULL;
  }

  GFile *trash = g_file_new_for_uri ("trash:");

  GFileEnumerator *enumerator = g_file_enumerate_children (trash,
      G_FILE_ATTRIBUTE_STANDARD_NAME
      ","
      G_FILE_ATTRIBUTE_TIME_MODIFIED
      ",trash::orig-path", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, FALSE, NULL);

  GHashTable *to_restore = g_hash_table_new_full (g_direct_hash,
      g_direct_equal, g_object_unref, g_free);

  if (enumerator) {
    GFileInfo *info;
    while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) {
      /* Retrieve the original file uri */
      const char *origpath = g_file_info_get_attribute_byte_string (info, "trash::orig-path");
      GFile *origfile = g_file_new_for_path (origpath);
      char *origuri = g_file_get_uri (origfile);
      g_object_unref (origfile);

      gboolean origuri_inserted = FALSE;
      gpointer lookupvalue = g_hash_table_lookup (trashed, origuri);

      if (lookupvalue) {
        guint64 *mtime = (guint64 *) lookupvalue;
        guint64 mtime_item = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
        if (*mtime == mtime_item) {
          GFile *item = g_file_get_child (trash, g_file_info_get_name (info)); /* File in the trash */
          g_hash_table_insert (to_restore, item, origuri);
          origuri_inserted = TRUE;
        }
      }

      if (!origuri_inserted) {
        g_free (origuri);
      }
    }

    g_file_enumerator_close (enumerator, FALSE, NULL);
    g_object_unref (enumerator);
  }

  g_object_unref (trash);

  return to_restore;
}

/** ---------------------------------------------------------------- */