/*
 * This file Copyright (C) 2009-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: file-list.c 14241 2014-01-21 03:10:30Z jordan $
 */

#include <limits.h> /* INT_MAX */
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include <libtransmission/transmission.h>
#include <libtransmission/utils.h>

#include "file-list.h"
#include "hig.h"
#include "icons.h"
#include "tr-prefs.h"
#include "util.h"

#define TR_DOWNLOAD_KEY  "tr-download-key"
#define TR_COLUMN_ID_KEY "tr-model-column-id-key"
#define TR_PRIORITY_KEY  "tr-priority-key"

enum
{
  /* these two fields could be any number at all so long as they're not
   * TR_PRI_LOW, TR_PRI_NORMAL, TR_PRI_HIGH, TRUE, or FALSE */
  NOT_SET = 1000,
  MIXED = 1001
};

enum
{
  FC_ICON,
  FC_LABEL,
  FC_LABEL_ESC,
  FC_PROG,
  FC_INDEX,
  FC_SIZE,
  FC_SIZE_STR,
  FC_HAVE,
  FC_PRIORITY,
  FC_ENABLED,
  N_FILE_COLS
};

typedef struct
{
  TrCore        * core;
  GtkWidget     * top;
  GtkWidget     * view;
  GtkTreeModel  * model; /* same object as store, but recast */
  GtkTreeStore  * store; /* same object as model, but recast */
  int             torrentId;
  guint           timeout_tag;
}
FileData;

static void
clearData (FileData * data)
{
  data->torrentId = -1;

  if (data->timeout_tag)
    {
      g_source_remove (data->timeout_tag);
      data->timeout_tag = 0;
    }
}

static void
freeData (gpointer data)
{
  clearData (data);
  g_free (data);
}

/***
****
***/

struct RefreshData
{
  int sort_column_id;
  gboolean resort_needed;

  tr_file_stat  * refresh_file_stat;
  tr_torrent * tor;

  FileData * file_data;
};

static gboolean
refreshFilesForeach (GtkTreeModel * model,
                     GtkTreePath  * path UNUSED,
                     GtkTreeIter  * iter,
                     gpointer       gdata)
{
  struct RefreshData * refresh_data = gdata;
  FileData * data = refresh_data->file_data;
  unsigned int index;
  uint64_t size;
  uint64_t old_have;
  int old_prog;
  int old_priority;
  int old_enabled;
  const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter);

  gtk_tree_model_get (model, iter, FC_ENABLED, &old_enabled,
                                   FC_PRIORITY, &old_priority,
                                   FC_INDEX, &index,
                                   FC_HAVE, &old_have,
                                   FC_SIZE, &size,
                                   FC_PROG, &old_prog,
                                   -1);

  if (is_file)
    {
      tr_torrent * tor = refresh_data->tor;
      const tr_info * inf = tr_torrentInfo (tor);
      const int enabled = !inf->files[index].dnd;
      const int priority = inf->files[index].priority;
      const uint64_t have = refresh_data->refresh_file_stat[index].bytesCompleted;
      const int prog = size ? (int)((100.0*have)/size) : 1;

      if ((priority!=old_priority) || (enabled!=old_enabled) || (have!=old_have) || (prog!=old_prog))
        {
          /* Changing a value in the sort column can trigger a resort
           * which breaks this foreach () call. (See #3529)
           * As a workaround: if that's about to happen, temporarily disable
           * sorting until we finish walking the tree. */
          if (!refresh_data->resort_needed)
            {
              if ((refresh_data->resort_needed =
                  ((refresh_data->sort_column_id==FC_PRIORITY) && (priority!=old_priority)) ||
                  ((refresh_data->sort_column_id==FC_ENABLED) && (enabled!=old_enabled))))
                {
                  refresh_data->resort_needed = TRUE;
                  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (data->model),
                                                        GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
                                                        GTK_SORT_ASCENDING);
                }
            }

          gtk_tree_store_set (data->store, iter, FC_PRIORITY, priority,
                                                 FC_ENABLED, enabled,
                                                 FC_HAVE, have,
                                                 FC_PROG, prog,
                                                 -1);
        }
    }
  else
    {
      GtkTreeIter child;
      uint64_t sub_size = 0;
      uint64_t have = 0;
      int prog;
      int enabled = NOT_SET;
      int priority = NOT_SET;

      /* since gtk_tree_model_foreach () is depth-first, we can
       * get the `sub' info by walking the immediate children */

      if (gtk_tree_model_iter_children (model, &child, iter)) do
        {
          int child_enabled;
          int child_priority;
          int64_t child_have, child_size;

          gtk_tree_model_get (model, &child, FC_SIZE, &child_size,
                                             FC_HAVE, &child_have,
                                             FC_PRIORITY, &child_priority,
                                             FC_ENABLED, &child_enabled,
                                             -1);

          if ((child_enabled != FALSE) && (child_enabled != NOT_SET))
            {
              sub_size += child_size;
              have += child_have;
            }

          if (enabled == NOT_SET)
            enabled = child_enabled;
          else if (enabled != child_enabled)
            enabled = MIXED;

          if (priority == NOT_SET)
            priority = child_priority;
          else if (priority != child_priority)
            priority = MIXED;
        }
      while (gtk_tree_model_iter_next (model, &child));

      prog = sub_size ? (int)((100.0*have)/sub_size) : 1;

      if ((size!=sub_size) || (have!=old_have)
                             || (priority!=old_priority)
                             || (enabled!=old_enabled)
                             || (prog!=old_prog))
        {
          char size_str[64];
          tr_strlsize (size_str, sub_size, sizeof size_str);
          gtk_tree_store_set (data->store, iter, FC_SIZE, sub_size,
                                                 FC_SIZE_STR, size_str,
                                                 FC_HAVE, have,
                                                 FC_PRIORITY, priority,
                                                 FC_ENABLED, enabled,
                                                 FC_PROG, prog,
                                                 -1);
        }
    }

  return FALSE; /* keep walking */
}

static void
gtr_tree_model_foreach_postorder_subtree (GtkTreeModel            * model,
                                          GtkTreeIter             * parent,
                                          GtkTreeModelForeachFunc   func,
                                          gpointer                  data)
{
  GtkTreeIter child;
  if (gtk_tree_model_iter_children (model, &child, parent)) do
    gtr_tree_model_foreach_postorder_subtree (model, &child, func, data);
  while (gtk_tree_model_iter_next (model, &child));
  if (parent)
    func (model, NULL, parent, data);
}

static void
gtr_tree_model_foreach_postorder (GtkTreeModel            * model,
                                  GtkTreeModelForeachFunc   func,
                                  gpointer                  data)
{
  GtkTreeIter iter;
  if (gtk_tree_model_iter_nth_child (model, &iter, NULL, 0)) do
    gtr_tree_model_foreach_postorder_subtree (model, &iter, func, data);
  while (gtk_tree_model_iter_next (model, &iter));
}

static void
refresh (FileData * data)
{
  tr_torrent * tor = gtr_core_find_torrent (data->core, data->torrentId);

  if (tor == NULL)
    {
      gtr_file_list_clear (data->top);
    }
  else
    {
      GtkSortType order;
      int sort_column_id;
      tr_file_index_t fileCount;
      struct RefreshData refresh_data;
      GtkTreeSortable * sortable = GTK_TREE_SORTABLE (data->model);
      gtk_tree_sortable_get_sort_column_id (sortable, &sort_column_id, &order);

      refresh_data.sort_column_id = sort_column_id;
      refresh_data.resort_needed = FALSE;
      refresh_data.refresh_file_stat = tr_torrentFiles (tor, &fileCount);
      refresh_data.tor = tor;
      refresh_data.file_data = data;

      gtr_tree_model_foreach_postorder (data->model, refreshFilesForeach, &refresh_data);

      if (refresh_data.resort_needed)
        gtk_tree_sortable_set_sort_column_id (sortable, sort_column_id, order);

      tr_torrentFilesFree (refresh_data.refresh_file_stat, fileCount);
    }
}

static gboolean
refreshModel (gpointer file_data)
{
  refresh (file_data);

  return G_SOURCE_CONTINUE;
}

/***
****
***/

struct ActiveData
{
  GtkTreeSelection  * sel;
  GArray            * array;
};

static gboolean
getSelectedFilesForeach (GtkTreeModel * model,
                         GtkTreePath  * path UNUSED,
                         GtkTreeIter  * iter,
                         gpointer       gdata)
{
  const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter);

  if (is_file)
    {
      struct ActiveData * data = gdata;

      /* active means: if it's selected or any ancestor is selected */
      gboolean is_active = gtk_tree_selection_iter_is_selected (data->sel, iter);

      if (!is_active)
        {
          GtkTreeIter walk = *iter;
          GtkTreeIter parent;
          while (!is_active && gtk_tree_model_iter_parent (model, &parent, &walk))
            {
              is_active = gtk_tree_selection_iter_is_selected (data->sel, &parent);
              walk = parent;
            }
        }

      if (is_active)
        {
          unsigned int i;
          gtk_tree_model_get (model, iter, FC_INDEX, &i, -1);
          g_array_append_val (data->array, i);
        }
    }

  return FALSE; /* keep walking */
}

static GArray*
getSelectedFilesAndDescendants (GtkTreeView * view)
{
  struct ActiveData data;

  data.sel = gtk_tree_view_get_selection (view);
  data.array = g_array_new (FALSE, FALSE, sizeof (tr_file_index_t));
  gtk_tree_model_foreach (gtk_tree_view_get_model (view),
                          getSelectedFilesForeach, &data);
  return data.array;
}

struct SubtreeForeachData
{
  GArray       * array;
  GtkTreePath  * path;
};

static gboolean
getSubtreeForeach (GtkTreeModel   * model,
                   GtkTreePath    * path,
                   GtkTreeIter    * iter,
                   gpointer         gdata)
{
  const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter);

  if (is_file)
    {
      struct SubtreeForeachData * data = gdata;

      if (!gtk_tree_path_compare (path, data->path) || gtk_tree_path_is_descendant (path, data->path))
        {
          unsigned int i;
          gtk_tree_model_get (model, iter, FC_INDEX, &i, -1);
          g_array_append_val (data->array, i);
        }
    }

  return FALSE; /* keep walking */
}

static void
getSubtree (GtkTreeView * view, GtkTreePath * path, GArray * indices)
{
  struct SubtreeForeachData tmp;
  tmp.array = indices;
  tmp.path = path;
  gtk_tree_model_foreach (gtk_tree_view_get_model (view), getSubtreeForeach, &tmp);
}

/* if `path' is a selected row, all selected rows are returned.
 * otherwise, only the row indicated by `path' is returned.
 * this is for toggling all the selected rows' states in a batch.
 */
static GArray*
getActiveFilesForPath (GtkTreeView * view, GtkTreePath * path)
{
  GArray * indices;
  GtkTreeSelection * sel = gtk_tree_view_get_selection (view);

  if (gtk_tree_selection_path_is_selected (sel, path))
    {
      /* clicked in a selected row... use the current selection */
      indices = getSelectedFilesAndDescendants (view);
    }
  else
    {
      /* clicked OUTSIDE of the selected row... just use the clicked row */
      indices = g_array_new (FALSE, FALSE, sizeof (tr_file_index_t));
      getSubtree (view, path, indices);
    }

  return indices;
}

/***
****
***/

void
gtr_file_list_clear (GtkWidget * w)
{
  gtr_file_list_set_torrent (w, -1);
}

struct build_data
{
  GtkWidget    * w;
  tr_torrent   * tor;
  GtkTreeIter  * iter;
  GtkTreeStore * store;
};

struct row_struct
{
  uint64_t    length;
  char      * name;
  int         index;
};

static void
buildTree (GNode * node, gpointer gdata)
{
  char size_str[64];
  GtkTreeIter child_iter;
  struct build_data * build = gdata;
  struct row_struct *child_data = node->data;
  const gboolean isLeaf = node->children == NULL;

  const char * mime_type = isLeaf ? gtr_get_mime_type_from_filename (child_data->name) : DIRECTORY_MIME_TYPE;
  GdkPixbuf * icon = gtr_get_mime_type_icon (mime_type, GTK_ICON_SIZE_MENU, build->w);
  const tr_info * inf = tr_torrentInfo (build->tor);
  const int priority = isLeaf ? inf->files[ child_data->index ].priority : 0;
  const gboolean enabled = isLeaf ? !inf->files[ child_data->index ].dnd : TRUE;
  char * name_esc = g_markup_escape_text (child_data->name, -1);

  tr_strlsize (size_str, child_data->length, sizeof size_str);

  gtk_tree_store_insert_with_values (build->store, &child_iter, build->iter, INT_MAX,
                                     FC_INDEX, child_data->index,
                                     FC_LABEL, child_data->name,
                                     FC_LABEL_ESC, name_esc,
                                     FC_SIZE, child_data->length,
                                     FC_SIZE_STR, size_str,
                                     FC_ICON, icon,
                                     FC_PRIORITY, priority,
                                     FC_ENABLED, enabled,
                                     -1);

  if (!isLeaf)
    {
      struct build_data b = *build;
      b.iter = &child_iter;
      g_node_children_foreach (node, G_TRAVERSE_ALL, buildTree, &b);
    }

  g_free (name_esc);
  g_object_unref (icon);

  /* we're done with this node */
  g_free (child_data->name);
  g_free (child_data);
}

static GNode*
find_child (GNode* parent, const char * name)
{
  GNode * child = parent->children;

  while (child)
    {
      const struct row_struct * child_data = child->data;
      if ((*child_data->name == *name) && !g_strcmp0 (child_data->name, name))
        break;
      child = child->next;
    }

  return child;
}

void
gtr_file_list_set_torrent (GtkWidget * w, int torrentId)
{
  GtkTreeStore * store;
  FileData * data = g_object_get_data (G_OBJECT (w), "file-data");

  /* unset the old fields */
  clearData (data);

  /* instantiate the model */
  store = gtk_tree_store_new (N_FILE_COLS,
                              GDK_TYPE_PIXBUF,  /* icon */
                              G_TYPE_STRING,    /* label */
                              G_TYPE_STRING,    /* label esc */
                              G_TYPE_INT,       /* prog [0..100] */
                              G_TYPE_UINT,      /* index */
                              G_TYPE_UINT64,    /* size */
                              G_TYPE_STRING,    /* size str */
                              G_TYPE_UINT64,    /* have */
                              G_TYPE_INT,       /* priority */
                              G_TYPE_INT);     /* dl enabled */

  data->store = store;
  data->model = GTK_TREE_MODEL (store);
  data->torrentId = torrentId;

  /* populate the model */
  if (torrentId > 0)
    {
      tr_torrent * tor = gtr_core_find_torrent (data->core, torrentId);
      if (tor != NULL)
        {
          tr_file_index_t i;
          const tr_info * inf = tr_torrentInfo (tor);
          struct row_struct * root_data;
          GNode * root;
          struct build_data build;

          /* build a GNode tree of the files */
          root_data = g_new0 (struct row_struct, 1);
          root_data->name = g_strdup (tr_torrentName (tor));
          root_data->index = -1;
          root_data->length = 0;
          root = g_node_new (root_data);
          for (i=0; i<inf->fileCount; ++i)
            {
              int j;
              GNode * parent = root;
              const tr_file * file = &inf->files[i];
              char ** tokens = g_strsplit (file->name, G_DIR_SEPARATOR_S, 0);
              for (j=0; tokens[j]; ++j)
                {
                  const gboolean isLeaf = tokens[j+1] == NULL;
                  const char * name = tokens[j];
                  GNode * node = find_child (parent, name);
                  if (node == NULL)
                    {
                      struct row_struct * row = g_new (struct row_struct, 1);
                      row->name = g_strdup (name);
                      row->index = isLeaf ? (int)i : -1;
                      row->length = isLeaf ? file->length : 0;
                      node = g_node_new (row);
                      g_node_append (parent, node);
                    }
                  parent = node;
                }
              g_strfreev (tokens);
            }

          /* now, add them to the model */
          build.w = w;
          build.tor = tor;
          build.store = data->store;
          build.iter = NULL;
          g_node_children_foreach (root, G_TRAVERSE_ALL, buildTree, &build);

          /* cleanup */
          g_node_destroy (root);
          g_free (root_data->name);
          g_free (root_data);
        }

      refresh (data);
      data->timeout_tag = gdk_threads_add_timeout_seconds (SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, refreshModel, data);
    }

  gtk_tree_view_set_model (GTK_TREE_VIEW (data->view), data->model);
  gtk_tree_view_expand_all (GTK_TREE_VIEW (data->view));
  g_object_unref (data->model);
}

/***
****
***/

static void
renderDownload (GtkTreeViewColumn  * column UNUSED,
                GtkCellRenderer    * renderer,
                GtkTreeModel       * model,
                GtkTreeIter        * iter,
                gpointer             data   UNUSED)
{
  gboolean enabled;
  gtk_tree_model_get (model, iter, FC_ENABLED, &enabled, -1);
  g_object_set (renderer, "inconsistent", (enabled==MIXED),
                          "active", (enabled==TRUE),
                          NULL);
}

static void
renderPriority (GtkTreeViewColumn  * column UNUSED,
                GtkCellRenderer    * renderer,
                GtkTreeModel       * model,
                GtkTreeIter        * iter,
                gpointer             data   UNUSED)
{
  int priority;
  const char * text;
  gtk_tree_model_get (model, iter, FC_PRIORITY, &priority, -1);
  switch (priority)
    {
      case TR_PRI_HIGH:   text = _("High"); break;
      case TR_PRI_NORMAL: text = _("Normal"); break;
      case TR_PRI_LOW:    text = _("Low"); break;
      default:            text = _("Mixed"); break;
    }
  g_object_set (renderer, "text", text, NULL);
}

/* build a filename from tr_torrentGetCurrentDir () + the model's FC_LABELs */
static char*
buildFilename (tr_torrent * tor, GtkTreeModel * model,
               GtkTreePath * path, GtkTreeIter * iter)
{
  char * ret;
  GtkTreeIter child;
  GtkTreeIter parent = *iter;
  int n = gtk_tree_path_get_depth (path);
  char ** tokens = g_new0 (char*, n + 2);
  tokens[0] = g_strdup (tr_torrentGetCurrentDir (tor));
  do {
    child = parent;
    gtk_tree_model_get (model, &child, FC_LABEL, &tokens[n--], -1);
  } while (gtk_tree_model_iter_parent (model, &parent, &child));
  ret = g_build_filenamev (tokens);
  g_strfreev (tokens);
  return ret;
}

static gboolean
onRowActivated (GtkTreeView * view, GtkTreePath * path,
                GtkTreeViewColumn * col UNUSED, gpointer gdata)
{
  gboolean handled = FALSE;
  FileData * data = gdata;
  tr_torrent * tor = gtr_core_find_torrent (data->core, data->torrentId);

  if (tor != NULL)
    {
      GtkTreeIter iter;
      GtkTreeModel * model = gtk_tree_view_get_model (view);

      if (gtk_tree_model_get_iter (model, &iter, path))
        {
          int prog;
          char * filename = buildFilename (tor, model, path, &iter);
          gtk_tree_model_get (model, &iter, FC_PROG, &prog, -1);

          /* if the file's not done, walk up the directory tree until we find
           * an ancestor that exists, and open that instead */
          if (filename && (prog<100 || !g_file_test (filename, G_FILE_TEST_EXISTS))) do
            {
              char * tmp = g_path_get_dirname (filename);
              g_free (filename);
              filename = tmp;
            }
          while (filename && *filename && !g_file_test (filename, G_FILE_TEST_EXISTS));

          if ((handled = filename && *filename))
            gtr_open_file (filename);
        }
    }

  return handled;
}

static gboolean
onViewPathToggled (GtkTreeView       * view,
                   GtkTreeViewColumn * col,
                   GtkTreePath       * path,
                   FileData          * data)
{
  int cid;
  tr_torrent * tor;
  gboolean handled = FALSE;

  if (!col || !path)
    return FALSE;

  cid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (col), TR_COLUMN_ID_KEY));
  tor = gtr_core_find_torrent (data->core, data->torrentId);
  if ((tor != NULL) && ((cid == FC_PRIORITY) || (cid == FC_ENABLED)))
    {
      GtkTreeIter iter;
      GArray * indices = getActiveFilesForPath (view, path);
      GtkTreeModel * model = data->model;

      gtk_tree_model_get_iter (model, &iter, path);

      if (cid == FC_PRIORITY)
        {
          int priority;
          gtk_tree_model_get (model, &iter, FC_PRIORITY, &priority, -1);
          switch (priority)
            {
              case TR_PRI_NORMAL: priority = TR_PRI_HIGH; break;
              case TR_PRI_HIGH:   priority = TR_PRI_LOW; break;
              default:            priority = TR_PRI_NORMAL; break;
            }
          tr_torrentSetFilePriorities (tor,
                                       (tr_file_index_t *) indices->data,
                                       (tr_file_index_t) indices->len,
                                         priority);
        }
      else
        {
          int enabled;
          gtk_tree_model_get (model, &iter, FC_ENABLED, &enabled, -1);
          enabled = !enabled;

          tr_torrentSetFileDLs (tor,
                                (tr_file_index_t *) indices->data,
                                (tr_file_index_t) indices->len,
                                enabled);
        }

      refresh (data);
      g_array_free (indices, TRUE);
      handled = TRUE;
    }

  return handled;
}

/**
 * @note 'col' and 'path' are assumed not to be NULL.
 */
static gboolean
getAndSelectEventPath (GtkTreeView        * treeview,
                       GdkEventButton     * event,
                       GtkTreeViewColumn ** col,
                       GtkTreePath       ** path)
{
  GtkTreeSelection * sel;

  if (gtk_tree_view_get_path_at_pos (treeview,
                                     event->x, event->y,
                                     path, col, NULL, NULL))
    {
      sel = gtk_tree_view_get_selection (treeview);
      if (!gtk_tree_selection_path_is_selected (sel, *path))
        {
          gtk_tree_selection_unselect_all (sel);
          gtk_tree_selection_select_path (sel, *path);
        }
      return TRUE;
    }

  return FALSE;
}

static gboolean
onViewButtonPressed (GtkWidget * w, GdkEventButton * event, gpointer gdata)
{
  GtkTreeViewColumn * col;
  GtkTreePath * path = NULL;
  gboolean handled = FALSE;
  GtkTreeView * treeview = GTK_TREE_VIEW (w);
  FileData * data = gdata;

  if ((event->type == GDK_BUTTON_PRESS)
       && (event->button == 1)
       && ! (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))
       && getAndSelectEventPath (treeview, event, &col, &path))
    {
      handled = onViewPathToggled (treeview, col, path, data);

      if (path != NULL)
        gtk_tree_path_free (path);
    }

  return handled;
}

struct rename_data
{
  int error;
  char * newname;
  char * path_string;
  FileData * file_data;
};

static int
on_rename_done_idle (struct rename_data * data)
{
  if (data->error == 0)
    {
      GtkTreeIter iter;

      if (gtk_tree_model_get_iter_from_string (data->file_data->model, &iter, data->path_string))
        gtk_tree_store_set (data->file_data->store, &iter, FC_LABEL, data->newname, -1);
    }
  else
    {
      GtkWidget * w = gtk_message_dialog_new (
        GTK_WINDOW (gtk_widget_get_toplevel(data->file_data->top)),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_ERROR,
        GTK_BUTTONS_CLOSE,
        _("Unable to rename file as \"%s\": %s"),
        data->newname,
        tr_strerror(data->error));
      gtk_message_dialog_format_secondary_text (
        GTK_MESSAGE_DIALOG (w), "%s",
        _("Please correct the errors and try again."));
      gtk_dialog_run (GTK_DIALOG (w));
      gtk_widget_destroy (w);
    }

  /* cleanup */
  g_free (data->path_string);
  g_free (data->newname);
  g_free (data);
  return G_SOURCE_REMOVE;
}

static void
on_rename_done (tr_torrent          * tor          G_GNUC_UNUSED,
                const char          * oldpath      G_GNUC_UNUSED,
                const char          * newname      G_GNUC_UNUSED,
                int                   error,
                struct rename_data  * rename_data)
{
  rename_data->error = error;
  gdk_threads_add_idle ((GSourceFunc)on_rename_done_idle, rename_data);
}

static void
cell_edited_callback (GtkCellRendererText * cell G_GNUC_UNUSED,
                      gchar               * path_string,
                      gchar               * newname,
                      FileData            * data)
{
  tr_torrent * tor;
  GString * oldpath;
  GtkTreeIter iter;
  struct rename_data * rename_data;

  tor = gtr_core_find_torrent (data->core, data->torrentId);
  if (tor == NULL)
    return;
  if (!gtk_tree_model_get_iter_from_string (data->model, &iter, path_string))
    return;

  /* build oldpath */
  oldpath = g_string_new (NULL);
  for (;;)
    {
      char * token = NULL;
      GtkTreeIter child;
      gtk_tree_model_get (data->model, &iter, FC_LABEL, &token, -1);
      g_string_prepend (oldpath, token);
      g_free (token);

      child = iter;
      if (!gtk_tree_model_iter_parent (data->model, &iter, &child))
        break;

      g_string_prepend_c (oldpath, G_DIR_SEPARATOR);
    }

  /* do the renaming */
  rename_data = g_new0 (struct rename_data, 1);
  rename_data->newname = g_strdup (newname);
  rename_data->file_data = data;
  rename_data->path_string = g_strdup (path_string);
  tr_torrentRenamePath (tor, oldpath->str, newname, (tr_torrent_rename_done_func)on_rename_done, rename_data);

  /* cleanup */
  g_string_free (oldpath, TRUE);
}


GtkWidget *
gtr_file_list_new (TrCore * core, int torrentId)
{
  int size;
  int width;
  GtkWidget * ret;
  GtkWidget * view;
  GtkWidget * scroll;
  GtkCellRenderer * rend;
  GtkTreeSelection * sel;
  GtkTreeViewColumn * col;
  GtkTreeView * tree_view;
  const char * title;
  PangoLayout * pango_layout;
  PangoContext * pango_context;
  PangoFontDescription * pango_font_description;
  FileData * data = g_new0 (FileData, 1);

  data->core = core;

  /* create the view */
  view = gtk_tree_view_new ();
  tree_view = GTK_TREE_VIEW (view);
  gtk_tree_view_set_rules_hint (tree_view, TRUE);
  gtk_container_set_border_width (GTK_CONTAINER (view), GUI_PAD_BIG);
  g_signal_connect (view, "button-press-event",
                    G_CALLBACK (onViewButtonPressed), data);
  g_signal_connect (view, "row_activated",
                    G_CALLBACK (onRowActivated), data);
  g_signal_connect (view, "button-release-event",
                    G_CALLBACK (on_tree_view_button_released), NULL);


  pango_context = gtk_widget_create_pango_context (view);
  pango_font_description = pango_font_description_copy (pango_context_get_font_description (pango_context));
  size = pango_font_description_get_size (pango_font_description);
  pango_font_description_set_size (pango_font_description, size * 0.8);
  g_object_unref (G_OBJECT (pango_context));

  /* set up view */
  sel = gtk_tree_view_get_selection (tree_view);
  gtk_tree_selection_set_mode (sel, GTK_SELECTION_MULTIPLE);
  gtk_tree_view_expand_all (tree_view);
  gtk_tree_view_set_search_column (tree_view, FC_LABEL);

  /* add file column */
  col = GTK_TREE_VIEW_COLUMN (g_object_new (GTK_TYPE_TREE_VIEW_COLUMN,
                                            "expand", TRUE,
                                            "title", _("Name"),
                                            NULL));
  gtk_tree_view_column_set_resizable (col, TRUE);
  rend = gtk_cell_renderer_pixbuf_new ();
  gtk_tree_view_column_pack_start (col, rend, FALSE);
  gtk_tree_view_column_add_attribute (col, rend, "pixbuf", FC_ICON);
  /* add text renderer */
  rend = gtk_cell_renderer_text_new ();
  g_object_set (rend, "editable", TRUE, NULL);
  g_object_set (rend, "ellipsize", PANGO_ELLIPSIZE_END, "font-desc", pango_font_description, NULL);
  g_signal_connect (rend, "edited", (GCallback)cell_edited_callback, data);
  gtk_tree_view_column_pack_start (col, rend, TRUE);
  gtk_tree_view_column_set_attributes (col, rend, "text", FC_LABEL, NULL);
  gtk_tree_view_column_set_sort_column_id (col, FC_LABEL);
  gtk_tree_view_append_column (tree_view, col);

  /* add "size" column */
  title = _("Size");
  rend = gtk_cell_renderer_text_new ();
  g_object_set (rend, "alignment", PANGO_ALIGN_RIGHT,
                      "font-desc", pango_font_description,
                      "xpad", GUI_PAD,
                      "xalign", 1.0f,
                      "yalign", 0.5f,
                      NULL);
  col = gtk_tree_view_column_new_with_attributes (title, rend, NULL);
  gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
  gtk_tree_view_column_set_sort_column_id (col, FC_SIZE);
  gtk_tree_view_column_set_attributes (col, rend, "text", FC_SIZE_STR, NULL);
  gtk_tree_view_append_column (tree_view, col);

  /* add "progress" column */
  title = _("Have");
  pango_layout = gtk_widget_create_pango_layout (view, title);
  pango_layout_get_pixel_size (pango_layout, &width, NULL);
  width += 30; /* room for the sort indicator */
  g_object_unref (G_OBJECT (pango_layout));
  rend = gtk_cell_renderer_progress_new ();
  col = gtk_tree_view_column_new_with_attributes (title, rend, "value", FC_PROG, NULL);
  gtk_tree_view_column_set_fixed_width (col, width);
  gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED);
  gtk_tree_view_column_set_sort_column_id (col, FC_PROG);
  gtk_tree_view_append_column (tree_view, col);

  /* add "enabled" column */
  title = _("Download");
  pango_layout = gtk_widget_create_pango_layout (view, title);
  pango_layout_get_pixel_size (pango_layout, &width, NULL);
  width += 30; /* room for the sort indicator */
  g_object_unref (G_OBJECT (pango_layout));
  rend = gtk_cell_renderer_toggle_new ();
  col = gtk_tree_view_column_new_with_attributes (title, rend, NULL);
  g_object_set_data (G_OBJECT (col), TR_COLUMN_ID_KEY,
                     GINT_TO_POINTER (FC_ENABLED));
  gtk_tree_view_column_set_fixed_width (col, width);
  gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED);
  gtk_tree_view_column_set_cell_data_func (col, rend, renderDownload, NULL, NULL);
  gtk_tree_view_column_set_sort_column_id (col, FC_ENABLED);
  gtk_tree_view_append_column (tree_view, col);

  /* add priority column */
  title = _("Priority");
  pango_layout = gtk_widget_create_pango_layout (view, title);
  pango_layout_get_pixel_size (pango_layout, &width, NULL);
  width += 30; /* room for the sort indicator */
  g_object_unref (G_OBJECT (pango_layout));
  rend = gtk_cell_renderer_text_new ();
  g_object_set (rend, "xalign", (gfloat)0.5, "yalign", (gfloat)0.5, NULL);
  col = gtk_tree_view_column_new_with_attributes (title, rend, NULL);
  g_object_set_data (G_OBJECT (col), TR_COLUMN_ID_KEY,
                     GINT_TO_POINTER (FC_PRIORITY));
  gtk_tree_view_column_set_fixed_width (col, width);
  gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED);
  gtk_tree_view_column_set_sort_column_id (col, FC_PRIORITY);
  gtk_tree_view_column_set_cell_data_func (col, rend, renderPriority, NULL, NULL);
  gtk_tree_view_append_column (tree_view, col);

  /* add tooltip to tree */
  gtk_tree_view_set_tooltip_column (tree_view, FC_LABEL_ESC);

  /* create the scrolled window and stick the view in it */
  scroll = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll),
                                        GTK_SHADOW_IN);
  gtk_container_add (GTK_CONTAINER (scroll), view);
  gtk_widget_set_size_request (scroll, -1, 200);

  ret = scroll;
  data->view = view;
  data->top = scroll;
  g_object_set_data_full (G_OBJECT (ret), "file-data", data, freeData);
  gtr_file_list_set_torrent (ret, torrentId);

  pango_font_description_free (pango_font_description);
  return ret;
}
