/******************************************************************************
 * $Id: main.c 14242 2014-01-21 03:15:33Z jordan $
 *
 * Copyright (c) Transmission authors and contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *****************************************************************************/

#include <locale.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h> /* exit () */
#include <time.h>

#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <gtk/gtk.h>

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

#include "actions.h"
#include "conf.h"
#include "details.h"
#include "dialogs.h"
#include "hig.h"
#include "makemeta-ui.h"
#include "msgwin.h"
#include "notify.h"
#include "open-dialog.h"
#include "relocate.h"
#include "stats.h"
#include "tr-core.h"
#include "tr-icon.h"
#include "tr-prefs.h"
#include "tr-window.h"
#include "util.h"

#define MY_CONFIG_NAME "transmission"
#define MY_READABLE_NAME "transmission-gtk"

#define TR_RESOURCE_PATH "/com/transmissionbt/transmission/"

#define SHOW_LICENSE
static const char * LICENSE =
  "Copyright 2005-2014. All code is copyrighted by the respective authors.\n"
  "\n"           
  "Transmission can be redistributed and/or modified under the terms of the "
  "GNU GPL versions 2 or 3 or by any future license endorsed by Mnemosyne LLC.\n"
  "\n"
  "In addition, linking to and/or using OpenSSL is allowed.\n"
  "\n"
  "This program is distributed in the hope that it will be useful, "
  "but WITHOUT ANY WARRANTY; without even the implied warranty of "
  "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
  "\n"
  "Some of Transmission's source files have more permissive licenses. "
  "Those files may, of course, be used on their own under their own terms.\n";

struct cbdata
{
  char * config_dir;
  gboolean start_paused;
  gboolean is_iconified;
  gboolean is_closing;

  guint activation_count;
  guint timer;
  guint update_model_soon_tag;
  guint refresh_actions_tag;
  gpointer icon;
  GtkWindow * wind;
  TrCore * core;
  GtkWidget * msgwin;
  GtkWidget * prefs;
  GSList * error_list;
  GSList * duplicates_list;
  GSList * details;
  GtkTreeSelection * sel;
};

static void
gtr_window_present (GtkWindow * window)
{
  gtk_window_present_with_time (window, gtk_get_current_event_time ());
}

/***
****
****  DETAILS DIALOGS MANAGEMENT
****
***/

static int
compare_integers (gconstpointer a, gconstpointer b)
{
  return GPOINTER_TO_INT (a) - GPOINTER_TO_INT (b);
}

static char*
get_details_dialog_key (GSList * id_list)
{
  GSList * l;
  GSList * tmp = g_slist_sort (g_slist_copy (id_list), compare_integers);
  GString * gstr = g_string_new (NULL);

  for (l=tmp; l!=NULL; l=l->next)
    g_string_append_printf (gstr, "%d ", GPOINTER_TO_INT (l->data));

  g_slist_free (tmp);
  return g_string_free (gstr, FALSE);
}

static void
get_selected_torrent_ids_foreach (GtkTreeModel  * model,
                                  GtkTreePath   * p UNUSED,
                                  GtkTreeIter   * iter,
                                  gpointer        gdata)
{
  int id;
  GSList ** ids = gdata;
  gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
  *ids = g_slist_append (*ids, GINT_TO_POINTER (id));
}
static GSList*
get_selected_torrent_ids (struct cbdata * data)
{
  GSList * ids = NULL;
  gtk_tree_selection_selected_foreach (data->sel,
                                       get_selected_torrent_ids_foreach,
                                       &ids);
  return ids;
}

static void
on_details_dialog_closed (gpointer gdata, GObject * dead)
{
  struct cbdata * data = gdata;

  data->details = g_slist_remove (data->details, dead);
}

static void
show_details_dialog_for_selected_torrents (struct cbdata * data)
{
  GtkWidget * dialog = NULL;
  GSList * l;
  GSList * ids = get_selected_torrent_ids (data);
  char * key = get_details_dialog_key (ids);

  for (l=data->details; dialog==NULL && l!=NULL; l=l->next)
    if (!g_strcmp0 (key, g_object_get_data (l->data, "key")))
      dialog = l->data;

  if (dialog == NULL)
    {
      dialog = gtr_torrent_details_dialog_new (GTK_WINDOW (data->wind), data->core);
      gtr_torrent_details_dialog_set_torrents (dialog, ids);
      g_object_set_data_full (G_OBJECT (dialog), "key", g_strdup (key), g_free);
      g_object_weak_ref (G_OBJECT (dialog), on_details_dialog_closed, data);
      data->details = g_slist_append (data->details, dialog);
      gtk_widget_show (dialog);
    }

  gtr_window_present (GTK_WINDOW (dialog));
  g_free (key);
  g_slist_free (ids);
}

/****
*****
*****  ON SELECTION CHANGED
*****
****/

struct counts_data
{
  int total_count;
  int queued_count;
  int stopped_count;
};

static void
get_selected_torrent_counts_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
                                     GtkTreeIter * iter, gpointer user_data)
{
  int activity = 0;
  struct counts_data * counts = user_data;

  ++counts->total_count;

  gtk_tree_model_get (model, iter, MC_ACTIVITY, &activity, -1);

  if ((activity == TR_STATUS_DOWNLOAD_WAIT) || (activity == TR_STATUS_SEED_WAIT))
    ++counts->queued_count;

  if (activity == TR_STATUS_STOPPED)
    ++counts->stopped_count;
}

static void
get_selected_torrent_counts (struct cbdata * data, struct counts_data * counts)
{
  counts->total_count = 0;
  counts->queued_count = 0;
  counts->stopped_count = 0;

  gtk_tree_selection_selected_foreach (data->sel, get_selected_torrent_counts_foreach, counts);
}

static void
count_updatable_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
                         GtkTreeIter * iter, gpointer accumulated_status)
{
  tr_torrent * tor;
  gtk_tree_model_get (model, iter, MC_TORRENT, &tor, -1);
  *(int*)accumulated_status |= tr_torrentCanManualUpdate (tor);
}

static gboolean
refresh_actions (gpointer gdata)
{
  struct cbdata * data = gdata;

  if (!data->is_closing)
    {
      int canUpdate;
      struct counts_data sel_counts;
      const size_t total = gtr_core_get_torrent_count (data->core);
      const size_t active = gtr_core_get_active_torrent_count (data->core);
      const int torrent_count = gtk_tree_model_iter_n_children (gtr_core_model (data->core), NULL);
      bool has_selection;

      get_selected_torrent_counts (data, &sel_counts);
      has_selection = sel_counts.total_count > 0;

      gtr_action_set_sensitive ("select-all", torrent_count != 0);
      gtr_action_set_sensitive ("deselect-all", torrent_count != 0);
      gtr_action_set_sensitive ("pause-all-torrents", active != 0);
      gtr_action_set_sensitive ("start-all-torrents", active != total);

      gtr_action_set_sensitive ("torrent-stop", (sel_counts.stopped_count < sel_counts.total_count));
      gtr_action_set_sensitive ("torrent-start", (sel_counts.stopped_count) > 0);
      gtr_action_set_sensitive ("torrent-start-now", (sel_counts.stopped_count + sel_counts.queued_count) > 0);
      gtr_action_set_sensitive ("torrent-verify",          has_selection);
      gtr_action_set_sensitive ("remove-torrent",          has_selection);
      gtr_action_set_sensitive ("delete-torrent",          has_selection);
      gtr_action_set_sensitive ("relocate-torrent",        has_selection);
      gtr_action_set_sensitive ("queue-move-top",          has_selection);
      gtr_action_set_sensitive ("queue-move-up",           has_selection);
      gtr_action_set_sensitive ("queue-move-down",         has_selection);
      gtr_action_set_sensitive ("queue-move-bottom",       has_selection);
      gtr_action_set_sensitive ("show-torrent-properties", has_selection);
      gtr_action_set_sensitive ("open-torrent-folder", sel_counts.total_count == 1);
      gtr_action_set_sensitive ("copy-magnet-link-to-clipboard", sel_counts.total_count == 1);

      canUpdate = 0;
      gtk_tree_selection_selected_foreach (data->sel, count_updatable_foreach, &canUpdate);
      gtr_action_set_sensitive ("torrent-reannounce", canUpdate != 0);
    }

  data->refresh_actions_tag = 0;
  return G_SOURCE_REMOVE;
}

static void
refresh_actions_soon (gpointer gdata)
{
  struct cbdata * data = gdata;

  if (!data->is_closing && !data->refresh_actions_tag)
    data->refresh_actions_tag = gdk_threads_add_idle (refresh_actions, data);
}

static void
on_selection_changed (GtkTreeSelection * s UNUSED, gpointer gdata)
{
  refresh_actions_soon (gdata);
}

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

static gboolean
has_magnet_link_handler (void)
{
  GAppInfo * app_info = g_app_info_get_default_for_uri_scheme ("magnet");
  const gboolean has_handler = app_info != NULL;
  g_clear_object (&app_info);
  return has_handler;
}

static void
register_magnet_link_handler (void)
{
  GError * error;
  GAppInfo * app;
  const char * const content_type = "x-scheme-handler/magnet";

  error = NULL;
  app = g_app_info_create_from_commandline ("transmission-gtk",
                                            "transmission-gtk",
                                            G_APP_INFO_CREATE_SUPPORTS_URIS,
                                            &error);
  g_app_info_set_as_default_for_type (app, content_type, &error);
  if (error != NULL)
    {
      g_warning (_("Error registering Transmission as a %s handler: %s"),
                 content_type,
                 error->message);
      g_error_free (error);
    }

  g_clear_object (&app);
}
static void
ensure_magnet_handler_exists (void)
{
  if (!has_magnet_link_handler ())
    register_magnet_link_handler ();
}

static void
on_main_window_size_allocated (GtkWidget      * gtk_window,
                               GtkAllocation  * alloc UNUSED,
                               gpointer         gdata UNUSED)
{
  GdkWindow * gdk_window = gtk_widget_get_window (gtk_window);
  const gboolean isMaximized = (gdk_window != NULL)
                            && (gdk_window_get_state (gdk_window) & GDK_WINDOW_STATE_MAXIMIZED);

  gtr_pref_int_set (TR_KEY_main_window_is_maximized, isMaximized);

  if (!isMaximized)
    {
      int x, y, w, h;
      gtk_window_get_position (GTK_WINDOW (gtk_window), &x, &y);
      gtk_window_get_size (GTK_WINDOW (gtk_window), &w, &h);
      gtr_pref_int_set (TR_KEY_main_window_x, x);
      gtr_pref_int_set (TR_KEY_main_window_y, y);
      gtr_pref_int_set (TR_KEY_main_window_width, w);
      gtr_pref_int_set (TR_KEY_main_window_height, h);
    }
}

/***
**** listen to changes that come from RPC
***/

struct on_rpc_changed_struct
{
  TrCore * core;
  tr_rpc_callback_type type;
  int torrent_id;
};

static gboolean
on_rpc_changed_idle (gpointer gdata)
{
  tr_torrent * tor;
  struct on_rpc_changed_struct * data = gdata;

  switch (data->type)
    {
      case TR_RPC_SESSION_CLOSE:
        gtr_action_activate ("quit");
        break;

      case TR_RPC_TORRENT_ADDED:
        if ((tor = gtr_core_find_torrent (data->core, data->torrent_id)))
          gtr_core_add_torrent (data->core, tor, true);
        break;

      case TR_RPC_TORRENT_REMOVING:
        gtr_core_remove_torrent (data->core, data->torrent_id, false);
        break;

      case TR_RPC_TORRENT_TRASHING:
        gtr_core_remove_torrent (data->core, data->torrent_id, true);
        break;

      case TR_RPC_SESSION_CHANGED: {
        int i;
        tr_variant tmp;
        tr_variant * newval;
        tr_variant * oldvals = gtr_pref_get_all ();
        tr_quark key;
        GSList * l;
        GSList * changed_keys = NULL;
        tr_session * session = gtr_core_session (data->core);
        tr_variantInitDict (&tmp, 100);
        tr_sessionGetSettings (session, &tmp);
        for (i=0; tr_variantDictChild (&tmp, i, &key, &newval); ++i)
          {
            bool changed;
            tr_variant * oldval = tr_variantDictFind (oldvals, key);
            if (!oldval)
              {
                changed = true;
              }
            else
              {
                char * a = tr_variantToStr (oldval, TR_VARIANT_FMT_BENC, NULL);
                char * b = tr_variantToStr (newval, TR_VARIANT_FMT_BENC, NULL);
                changed = g_strcmp0 (a, b) != 0;
                tr_free (b);
                tr_free (a);
              }

            if (changed)
              changed_keys = g_slist_append (changed_keys, GINT_TO_POINTER(key));
          }
        tr_sessionGetSettings (session, oldvals);

        for (l=changed_keys; l!=NULL; l=l->next)
          gtr_core_pref_changed (data->core, GPOINTER_TO_INT(l->data));

        g_slist_free (changed_keys);
        tr_variantFree (&tmp);
        break;
      }

      case TR_RPC_TORRENT_CHANGED:
      case TR_RPC_TORRENT_MOVED:
      case TR_RPC_TORRENT_STARTED:
      case TR_RPC_TORRENT_STOPPED:
      case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
        /* nothing interesting to do here */
        break;
    }

  g_free (data);
  return G_SOURCE_REMOVE;
}

static tr_rpc_callback_status
on_rpc_changed (tr_session            * session G_GNUC_UNUSED,
                tr_rpc_callback_type    type,
                struct tr_torrent     * tor,
                void                  * gdata)
{
  struct cbdata * cbdata = gdata;
  struct on_rpc_changed_struct * data;

  data = g_new (struct on_rpc_changed_struct, 1);
  data->core = cbdata->core;
  data->type = type;
  data->torrent_id = tr_torrentId (tor);
  gdk_threads_add_idle (on_rpc_changed_idle, data);
  
  return TR_RPC_NOREMOVE;
}

/***
****  signal handling
***/

static sig_atomic_t global_sigcount = 0;
static struct cbdata * sighandler_cbdata = NULL;

static void
signal_handler (int sig)
{
  if (++global_sigcount > 1)
    {
      signal (sig, SIG_DFL);
      raise (sig);
    }
  else if ((sig == SIGINT) || (sig == SIGTERM))
    {
      g_message (_("Got signal %d; trying to shut down cleanly. Do it again if it gets stuck."), sig);
      gtr_actions_handler ("quit", sighandler_cbdata);
    }
}

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

static void app_setup (GtkWindow * wind, struct cbdata  * cbdata);

static void
on_startup (GApplication * application, gpointer user_data)
{
  GError * error;
  const char * str;
  GtkWindow * win;
  GtkUIManager * ui_manager;
  tr_session * session;
  struct cbdata * cbdata = user_data;

  signal (SIGINT, signal_handler);
  signal (SIGTERM, signal_handler);

  sighandler_cbdata = cbdata;

  /* ensure the directories are created */
  if ((str = gtr_pref_string_get (TR_KEY_download_dir)))
    g_mkdir_with_parents (str, 0777);
  if ((str = gtr_pref_string_get (TR_KEY_incomplete_dir)))
    g_mkdir_with_parents (str, 0777);

  /* initialize the libtransmission session */
  session = tr_sessionInit ("gtk", cbdata->config_dir, TRUE, gtr_pref_get_all ());

  gtr_pref_flag_set (TR_KEY_alt_speed_enabled, tr_sessionUsesAltSpeed (session));
  gtr_pref_int_set  (TR_KEY_peer_port, tr_sessionGetPeerPort (session));
  cbdata->core = gtr_core_new (session);

  /* init the ui manager */
  error = NULL;
  ui_manager = gtk_ui_manager_new ();
  gtr_actions_init (ui_manager, cbdata);
  gtk_ui_manager_add_ui_from_resource (ui_manager, TR_RESOURCE_PATH "transmission-ui.xml", &error);
  g_assert_no_error (error);
  gtk_ui_manager_ensure_update (ui_manager);

  /* create main window now to be a parent to any error dialogs */
  win = GTK_WINDOW (gtr_window_new (GTK_APPLICATION (application), ui_manager, cbdata->core));
  g_signal_connect (win, "size-allocate", G_CALLBACK (on_main_window_size_allocated), cbdata);
  g_application_hold (application);
  g_object_weak_ref (G_OBJECT (win), (GWeakNotify)g_application_release, application);
  app_setup (win, cbdata);
  tr_sessionSetRPCCallback (session, on_rpc_changed, cbdata);

  /* check & see if it's time to update the blocklist */
  if (gtr_pref_flag_get (TR_KEY_blocklist_enabled))
    {
      if (gtr_pref_flag_get (TR_KEY_blocklist_updates_enabled))
        {
          const int64_t last_time = gtr_pref_int_get (TR_KEY_blocklist_date);
          const int SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60;
          const time_t now = time (NULL);
          if (last_time + SECONDS_IN_A_WEEK < now)
            gtr_core_blocklist_update (cbdata->core);
        }
    }

  /* if there's no magnet link handler registered, register us */
  ensure_magnet_handler_exists ();
}

static void
on_activate (GApplication * app UNUSED, struct cbdata * cbdata)
{
  cbdata->activation_count++;

  /* GApplication emits an 'activate' signal when bootstrapping the primary.
   * Ordinarily we handle that by presenting the main window, but if the user
   * user started Transmission minimized, ignore that initial signal... */
  if (cbdata->is_iconified && (cbdata->activation_count == 1))
    return;

  gtr_action_activate ("present-main-window");
}

static void
open_files (GSList * files, gpointer gdata)
{
  struct cbdata * cbdata = gdata;
  const gboolean do_start = gtr_pref_flag_get (TR_KEY_start_added_torrents) && !cbdata->start_paused;
  const gboolean do_prompt = gtr_pref_flag_get (TR_KEY_show_options_window);
  const gboolean do_notify = TRUE;

  gtr_core_add_files (cbdata->core, files, do_start, do_prompt, do_notify);
}

static void
on_open (GApplication  * application UNUSED,
         GFile        ** f,
         gint            file_count,
         gchar         * hint UNUSED,
         gpointer        gdata)
{
  int i;
  GSList * files = NULL;

  for (i=0; i<file_count; i++)
    files = g_slist_prepend (files, f[i]);

  open_files (files, gdata);

  g_slist_free (files);
}

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

int
main (int argc, char ** argv)
{
  int ret;
  struct stat sb;
  char * application_id;
  GtkApplication * app;
  GOptionContext * option_context;
  bool show_version = false;
  GError * error = NULL;
  struct cbdata cbdata;

  GOptionEntry option_entries[] = {
    { "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &cbdata.config_dir, _("Where to look for configuration files"), NULL },
    { "paused",     'p', 0, G_OPTION_ARG_NONE, &cbdata.start_paused, _("Start with all torrents paused"), NULL },
    { "minimized",  'm', 0, G_OPTION_ARG_NONE, &cbdata.is_iconified, _("Start minimized in notification area"), NULL },
    { "version",    'v', 0, G_OPTION_ARG_NONE, &show_version, _("Show version number and exit"), NULL },
    { NULL, 0,   0, 0, NULL, NULL, NULL }
  };

  /* default settings */
  memset (&cbdata, 0, sizeof (struct cbdata));
  cbdata.config_dir = (char*) tr_getDefaultConfigDir (MY_CONFIG_NAME);

  /* init i18n */
  setlocale (LC_ALL, "");
  bindtextdomain (MY_READABLE_NAME, TRANSMISSIONLOCALEDIR);
  bind_textdomain_codeset (MY_READABLE_NAME, "UTF-8");
  textdomain (MY_READABLE_NAME);

  /* init glib/gtk */
#if !GLIB_CHECK_VERSION(2,35,4)
  g_type_init ();
#endif
  gtk_init (&argc, &argv);
  g_set_application_name (_("Transmission"));
  gtk_window_set_default_icon_name (MY_CONFIG_NAME);

  /* parse the command line */
  option_context = g_option_context_new (_("[torrent files or urls]"));
  g_option_context_add_main_entries (option_context, option_entries, GETTEXT_PACKAGE);
  g_option_context_set_translation_domain (option_context, GETTEXT_PACKAGE);
  if (!g_option_context_parse (option_context, &argc, &argv, &error))
    {
      g_print (_("%s\nRun '%s --help' to see a full list of available command line options.\n"), error->message, argv[0]);
      g_error_free (error);
      g_option_context_free (option_context);
      return 1;
    }
  g_option_context_free (option_context);

  /* handle the trivial "version" option */
  if (show_version)
    {
      fprintf (stderr, "%s %s\n", MY_READABLE_NAME, LONG_VERSION_STRING);
      return 0;
    }

  /* init the unit formatters */
  tr_formatter_mem_init (mem_K, _ (mem_K_str), _ (mem_M_str), _ (mem_G_str), _ (mem_T_str));
  tr_formatter_size_init (disk_K, _ (disk_K_str), _ (disk_M_str), _ (disk_G_str), _ (disk_T_str));
  tr_formatter_speed_init (speed_K, _ (speed_K_str), _ (speed_M_str), _ (speed_G_str), _ (speed_T_str));

  /* set up the config dir */
  gtr_pref_init (cbdata.config_dir);
  g_mkdir_with_parents (cbdata.config_dir, 0755);

  /* init notifications */
  gtr_notify_init ();

  /* init the application for the specified config dir */
  stat (cbdata.config_dir, &sb);
  application_id = g_strdup_printf ("com.transmissionbt.transmission_%lu_%lu", (unsigned long)sb.st_dev, (unsigned long)sb.st_ino);
  app = gtk_application_new (application_id, G_APPLICATION_HANDLES_OPEN);
  g_signal_connect (app, "open", G_CALLBACK (on_open), &cbdata);
  g_signal_connect (app, "startup", G_CALLBACK (on_startup), &cbdata);
  g_signal_connect (app, "activate", G_CALLBACK (on_activate), &cbdata);
  ret = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);
  g_free (application_id);
  return ret;
}

static void
on_core_busy (TrCore * core UNUSED, gboolean busy, struct cbdata * c)
{
  gtr_window_set_busy (c->wind, busy);
}

static void on_core_error (TrCore *, guint, const char *, struct cbdata *);
static void on_add_torrent (TrCore *, tr_ctor *, gpointer);
static void on_prefs_changed (TrCore * core, const tr_quark key, gpointer);
static void main_window_setup (struct cbdata * cbdata, GtkWindow * wind);
static gboolean update_model_loop (gpointer gdata);
static gboolean update_model_once (gpointer gdata);

static void
app_setup (GtkWindow * wind, struct cbdata * cbdata)
{
  if (cbdata->is_iconified)
    gtr_pref_flag_set (TR_KEY_show_notification_area_icon, TRUE);

  gtr_actions_set_core (cbdata->core);

  /* set up core handlers */
  g_signal_connect (cbdata->core, "busy", G_CALLBACK (on_core_busy), cbdata);
  g_signal_connect (cbdata->core, "add-error", G_CALLBACK (on_core_error), cbdata);
  g_signal_connect (cbdata->core, "add-prompt", G_CALLBACK (on_add_torrent), cbdata);
  g_signal_connect (cbdata->core, "prefs-changed", G_CALLBACK (on_prefs_changed), cbdata);

  /* add torrents from command-line and saved state */
  gtr_core_load (cbdata->core, cbdata->start_paused);
  gtr_core_torrents_added (cbdata->core);

  /* set up main window */
  main_window_setup (cbdata, wind);

  /* set up the icon */
  on_prefs_changed (cbdata->core, TR_KEY_show_notification_area_icon, cbdata);

  /* start model update timer */
  cbdata->timer = gdk_threads_add_timeout_seconds (MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata);
  update_model_once (cbdata);

  /* either show the window or iconify it */
  if (!cbdata->is_iconified)
    {
      gtk_widget_show (GTK_WIDGET (wind));
    }
  else
    {
      gtk_window_set_skip_taskbar_hint (cbdata->wind,
                                        cbdata->icon != NULL);
      cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies
      gtr_action_set_toggled ("toggle-main-window", FALSE);
    }

  if (!gtr_pref_flag_get (TR_KEY_user_has_given_informed_consent))
    {
      GtkWidget * w = gtk_message_dialog_new (GTK_WINDOW (wind),
                                              GTK_DIALOG_DESTROY_WITH_PARENT,
                                              GTK_MESSAGE_OTHER,
                                              GTK_BUTTONS_NONE,
                                              "%s",
        _("Transmission is a file sharing program. When you run a torrent, its data will be made available to others by means of upload. Any content you share is your sole responsibility."));
      gtk_dialog_add_button (GTK_DIALOG (w), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT);
      gtk_dialog_add_button (GTK_DIALOG (w), _("I _Agree"), GTK_RESPONSE_ACCEPT);
      gtk_dialog_set_default_response (GTK_DIALOG (w), GTK_RESPONSE_ACCEPT);
      switch (gtk_dialog_run (GTK_DIALOG (w)))
        {
          case GTK_RESPONSE_ACCEPT:
            /* only show it once */
            gtr_pref_flag_set (TR_KEY_user_has_given_informed_consent, TRUE);
            gtk_widget_destroy (w);
            break;

          default:
            exit (0);
        }
    }
}

static void
presentMainWindow (struct cbdata * cbdata)
{
  GtkWindow * window = cbdata->wind;

  if (cbdata->is_iconified)
    {
      cbdata->is_iconified = false;

      gtk_window_set_skip_taskbar_hint (window, FALSE);
    }

  if (!gtk_widget_get_visible (GTK_WIDGET (window)))
    {
      gtk_window_resize (window, gtr_pref_int_get (TR_KEY_main_window_width),
                                 gtr_pref_int_get (TR_KEY_main_window_height));
      gtk_window_move (window, gtr_pref_int_get (TR_KEY_main_window_x),
                               gtr_pref_int_get (TR_KEY_main_window_y));
      gtr_widget_set_visible (GTK_WIDGET (window), TRUE);
    }

  gtr_window_present (window);
  gdk_window_raise (gtk_widget_get_window (GTK_WIDGET(window)));
}

static void
hideMainWindow (struct cbdata * cbdata)
{
  GtkWindow * window = cbdata->wind;
  gtk_window_set_skip_taskbar_hint (window, TRUE);
  gtr_widget_set_visible (GTK_WIDGET (window), FALSE);
  cbdata->is_iconified = true;
}

static void
toggleMainWindow (struct cbdata * cbdata)
{
  if (cbdata->is_iconified)
    presentMainWindow (cbdata);
  else
    hideMainWindow (cbdata);
}

static void on_app_exit (gpointer vdata);

static gboolean
winclose (GtkWidget * w    UNUSED,
          GdkEvent * event UNUSED,
          gpointer         gdata)
{
  struct cbdata * cbdata = gdata;

  if (cbdata->icon != NULL)
    gtr_action_activate ("toggle-main-window");
  else
    on_app_exit (cbdata);

  return TRUE; /* don't propagate event further */
}

static void
rowChangedCB (GtkTreeModel  * model UNUSED,
              GtkTreePath   * path,
              GtkTreeIter   * iter  UNUSED,
              gpointer        gdata)
{
  struct cbdata * data = gdata;

  if (gtk_tree_selection_path_is_selected (data->sel, path))
    refresh_actions_soon (data);
}

static void
on_drag_data_received (GtkWidget         * widget          UNUSED,
                       GdkDragContext    * drag_context,
                       gint                x               UNUSED,
                       gint                y               UNUSED,
                       GtkSelectionData  * selection_data,
                       guint               info            UNUSED,
                       guint               time_,
                       gpointer            gdata)
{
  guint i;
  char ** uris = gtk_selection_data_get_uris (selection_data);
  const guint file_count = g_strv_length (uris);
  GSList * files = NULL;

  for (i=0; i<file_count; ++i)
    files = g_slist_prepend (files, g_file_new_for_uri (uris[i]));

  open_files (files, gdata);

  /* cleanup */
  g_slist_foreach (files, (GFunc)g_object_unref, NULL);
  g_slist_free (files);
  g_strfreev (uris);

  gtk_drag_finish (drag_context, true, FALSE, time_);
}

static void
main_window_setup (struct cbdata * cbdata, GtkWindow * wind)
{
  GtkWidget * w;
  GtkTreeModel * model;
  GtkTreeSelection * sel;

  g_assert (NULL == cbdata->wind);
  cbdata->wind = wind;
  cbdata->sel = sel = GTK_TREE_SELECTION (gtr_window_get_selection (cbdata->wind));

  g_signal_connect (sel, "changed", G_CALLBACK (on_selection_changed), cbdata);
  on_selection_changed (sel, cbdata);
  model = gtr_core_model (cbdata->core);
  g_signal_connect (model, "row-changed", G_CALLBACK (rowChangedCB), cbdata);
  g_signal_connect (wind, "delete-event", G_CALLBACK (winclose), cbdata);
  refresh_actions (cbdata);

  /* register to handle URIs that get dragged onto our main window */
  w = GTK_WIDGET (wind);
  gtk_drag_dest_set (w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
  gtk_drag_dest_add_uri_targets (w);
  g_signal_connect (w, "drag-data-received", G_CALLBACK (on_drag_data_received), cbdata);
}

static gboolean
on_session_closed (gpointer gdata)
{
  GSList * tmp;
  struct cbdata * cbdata = gdata;

  tmp = g_slist_copy (cbdata->details);
  g_slist_foreach (tmp, (GFunc)gtk_widget_destroy, NULL);
  g_slist_free (tmp);

  if (cbdata->prefs)
    gtk_widget_destroy (GTK_WIDGET (cbdata->prefs));
  if (cbdata->wind)
    gtk_widget_destroy (GTK_WIDGET (cbdata->wind));
  g_object_unref (cbdata->core);
  if (cbdata->icon)
    g_object_unref (cbdata->icon);
  g_slist_foreach (cbdata->error_list, (GFunc)g_free, NULL);
  g_slist_free (cbdata->error_list);
  g_slist_foreach (cbdata->duplicates_list, (GFunc)g_free, NULL);
  g_slist_free (cbdata->duplicates_list);

  return G_SOURCE_REMOVE;
}

struct session_close_struct
{
  tr_session * session;
  struct cbdata * cbdata;
};

/* since tr_sessionClose () is a blocking function,
 * delegate its call to another thread here... when it's done,
 * punt the GUI teardown back to the GTK+ thread */
static gpointer
session_close_threadfunc (gpointer gdata)
{
  struct session_close_struct * data = gdata;
  tr_sessionClose (data->session);
  gdk_threads_add_idle (on_session_closed, data->cbdata);
  g_free (data);
  return NULL;
}

static void
exit_now_cb (GtkWidget *w UNUSED, gpointer data UNUSED)
{
  exit (0);
}

static void
on_app_exit (gpointer vdata)
{
  GtkWidget *r, *p, *b, *w, *c;
  struct cbdata * cbdata = vdata;
  struct session_close_struct * session_close_data;

  if (cbdata->is_closing)
    return;

  cbdata->is_closing = true;

  /* stop the update timer */
  if (cbdata->timer)
    {
      g_source_remove (cbdata->timer);
      cbdata->timer = 0;
    }

  /* stop the refresh-actions timer */
  if (cbdata->refresh_actions_tag)
    {
      g_source_remove (cbdata->refresh_actions_tag);
      cbdata->refresh_actions_tag = 0;
    }

  c = GTK_WIDGET (cbdata->wind);
  gtk_container_remove (GTK_CONTAINER (c), gtk_bin_get_child (GTK_BIN (c)));

  r = gtk_alignment_new (0.5, 0.5, 0.01, 0.01);
  gtk_container_add (GTK_CONTAINER (c), r);

  p = gtk_grid_new ();
  gtk_grid_set_column_spacing (GTK_GRID (p), GUI_PAD_BIG);
  gtk_container_add (GTK_CONTAINER (r), p);

  w = gtk_image_new_from_stock (GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG);
  gtk_grid_attach (GTK_GRID (p), w, 0, 0, 1, 2);

  w = gtk_label_new (NULL);
  gtk_label_set_markup (GTK_LABEL (w), _("<b>Closing Connections</b>"));
  gtk_misc_set_alignment (GTK_MISC (w), 0.0, 0.5);
  gtk_grid_attach (GTK_GRID (p), w, 1, 0, 1, 1);

  w = gtk_label_new (_("Sending upload/download totals to tracker…"));
  gtk_misc_set_alignment (GTK_MISC (w), 0.0, 0.5);
  gtk_grid_attach (GTK_GRID (p), w, 1, 1, 1, 1);

  b = gtk_alignment_new (0.0, 1.0, 0.01, 0.01);
  w = gtk_button_new_with_mnemonic (_("_Quit Now"));
  g_signal_connect (w, "clicked", G_CALLBACK (exit_now_cb), NULL);
  gtk_container_add (GTK_CONTAINER (b), w);
  gtk_grid_attach (GTK_GRID (p), b, 1, 2, 1, 1);

  gtk_widget_show_all (r);
  gtk_widget_grab_focus (w);

  /* clear the UI */
  gtr_core_clear (cbdata->core);

  /* ensure the window is in its previous position & size.
   * this seems to be necessary because changing the main window's
   * child seems to unset the size */
  gtk_window_resize (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_width),
                                   gtr_pref_int_get (TR_KEY_main_window_height));
  gtk_window_move (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_x),
                                 gtr_pref_int_get (TR_KEY_main_window_y));

  /* shut down libT */
  session_close_data = g_new (struct session_close_struct, 1);
  session_close_data->cbdata = cbdata;
  session_close_data->session = gtr_core_close (cbdata->core);
  g_thread_new ("shutdown-thread", session_close_threadfunc, session_close_data);
}

static void
show_torrent_errors (GtkWindow * window, const char * primary, GSList ** files)
{
  GSList * l;
  GtkWidget * w;
  GString * s = g_string_new (NULL);
  const char * leader = g_slist_length (*files) > 1
                      ? gtr_get_unicode_string (GTR_UNICODE_BULLET)
                      : "";

  for (l=*files; l!=NULL; l=l->next)
    g_string_append_printf (s, "%s %s\n", leader, (const char*)l->data);

  w = gtk_message_dialog_new (window,
                              GTK_DIALOG_DESTROY_WITH_PARENT,
                              GTK_MESSAGE_ERROR,
                              GTK_BUTTONS_CLOSE,
                              "%s", primary);
  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (w),
                                            "%s", s->str);
  g_signal_connect_swapped (w, "response",
                            G_CALLBACK (gtk_widget_destroy), w);
  gtk_widget_show (w);
  g_string_free (s, TRUE);

  g_slist_foreach (*files, (GFunc)g_free, NULL);
  g_slist_free (*files);
  *files = NULL;
}

static void
flush_torrent_errors (struct cbdata * cbdata)
{
  if (cbdata->error_list)
    show_torrent_errors (cbdata->wind,
                         ngettext ("Couldn't add corrupt torrent",
                                   "Couldn't add corrupt torrents",
                                   g_slist_length (cbdata->error_list)),
                         &cbdata->error_list);

  if (cbdata->duplicates_list)
    show_torrent_errors (cbdata->wind,
                         ngettext ("Couldn't add duplicate torrent",
                                   "Couldn't add duplicate torrents",
                                   g_slist_length (cbdata->duplicates_list)),
                         &cbdata->duplicates_list);
}

static void
on_core_error (TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c)
{
  switch (code)
    {
      case TR_PARSE_ERR:
        c->error_list = g_slist_append (c->error_list, g_path_get_basename (msg));
        break;

      case TR_PARSE_DUPLICATE:
        c->duplicates_list = g_slist_append (c->duplicates_list, g_strdup (msg));
        break;

      case TR_CORE_ERR_NO_MORE_TORRENTS:
        flush_torrent_errors (c);
        break;

      default:
        g_assert_not_reached ();
        break;
    }
}

static gboolean
on_main_window_focus_in (GtkWidget      * widget UNUSED,
                         GdkEventFocus  * event  UNUSED,
                         gpointer         gdata)
{
  struct cbdata * cbdata = gdata;

  if (cbdata->wind)
    gtk_window_set_urgency_hint (cbdata->wind, FALSE);

  return FALSE;
}

static void
on_add_torrent (TrCore * core, tr_ctor * ctor, gpointer gdata)
{
  struct cbdata * cbdata = gdata;
  GtkWidget * w = gtr_torrent_options_dialog_new (cbdata->wind, core, ctor);

  g_signal_connect (w, "focus-in-event",
                    G_CALLBACK (on_main_window_focus_in),  cbdata);
  if (cbdata->wind)
    gtk_window_set_urgency_hint (cbdata->wind, TRUE);

  gtk_widget_show (w);
}

static void
on_prefs_changed (TrCore * core UNUSED, const tr_quark key, gpointer data)
{
  struct cbdata * cbdata = data;
  tr_session * tr = gtr_core_session (cbdata->core);

  switch (key)
    {
      case TR_KEY_encryption:
        tr_sessionSetEncryption (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_download_dir:
        tr_sessionSetDownloadDir (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_message_level:
        tr_logSetLevel (gtr_pref_int_get (key));
        break;

      case TR_KEY_peer_port:
        tr_sessionSetPeerPort (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_blocklist_enabled:
        tr_blocklistSetEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_blocklist_url:
        tr_blocklistSetURL (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_show_notification_area_icon:
        {
          const bool show = gtr_pref_flag_get (key);
          if (show && !cbdata->icon)
            cbdata->icon = gtr_icon_new (cbdata->core);
          else if (!show && cbdata->icon)
            g_clear_object (&cbdata->icon);
          break;
        }

      case TR_KEY_speed_limit_down_enabled:
        tr_sessionLimitSpeed (tr, TR_DOWN, gtr_pref_flag_get (key));
        break;

      case TR_KEY_speed_limit_down:
        tr_sessionSetSpeedLimit_KBps (tr, TR_DOWN, gtr_pref_int_get (key));
        break;

      case TR_KEY_speed_limit_up_enabled:
        tr_sessionLimitSpeed (tr, TR_UP, gtr_pref_flag_get (key));
        break;

      case TR_KEY_speed_limit_up:
        tr_sessionSetSpeedLimit_KBps (tr, TR_UP, gtr_pref_int_get (key));
        break;

      case TR_KEY_ratio_limit_enabled:
        tr_sessionSetRatioLimited (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_ratio_limit:
        tr_sessionSetRatioLimit (tr, gtr_pref_double_get (key));
        break;

      case TR_KEY_idle_seeding_limit:
        tr_sessionSetIdleLimit (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_idle_seeding_limit_enabled:
        tr_sessionSetIdleLimited (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_port_forwarding_enabled:
        tr_sessionSetPortForwardingEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_pex_enabled:
        tr_sessionSetPexEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_rename_partial_files:
        tr_sessionSetIncompleteFileNamingEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_download_queue_size:
        tr_sessionSetQueueSize (tr, TR_DOWN, gtr_pref_int_get (key));
        break;

      case TR_KEY_queue_stalled_minutes:
        tr_sessionSetQueueStalledMinutes (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_dht_enabled:
        tr_sessionSetDHTEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_utp_enabled:
        tr_sessionSetUTPEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_lpd_enabled:
        tr_sessionSetLPDEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_rpc_port:
        tr_sessionSetRPCPort (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_rpc_enabled:
        tr_sessionSetRPCEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_rpc_whitelist:
        tr_sessionSetRPCWhitelist (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_rpc_whitelist_enabled:
        tr_sessionSetRPCWhitelistEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_rpc_username:
        tr_sessionSetRPCUsername (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_rpc_password:
        tr_sessionSetRPCPassword (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_rpc_authentication_required:
        tr_sessionSetRPCPasswordEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_alt_speed_up:
        tr_sessionSetAltSpeed_KBps (tr, TR_UP, gtr_pref_int_get (key));
        break;

      case TR_KEY_alt_speed_down:
        tr_sessionSetAltSpeed_KBps (tr, TR_DOWN, gtr_pref_int_get (key));
        break;

      case TR_KEY_alt_speed_enabled:
        {
          const bool b = gtr_pref_flag_get (key);
          tr_sessionUseAltSpeed (tr, b);
          gtr_action_set_toggled (tr_quark_get_string(key,NULL), b);
          break;
        }

      case TR_KEY_alt_speed_time_begin:
        tr_sessionSetAltSpeedBegin (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_alt_speed_time_end:
        tr_sessionSetAltSpeedEnd (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_alt_speed_time_enabled:
        tr_sessionUseAltSpeedTime (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_alt_speed_time_day:
        tr_sessionSetAltSpeedDay (tr, gtr_pref_int_get (key));
        break;

      case TR_KEY_peer_port_random_on_start:
        tr_sessionSetPeerPortRandomOnStart (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_incomplete_dir:
        tr_sessionSetIncompleteDir (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_incomplete_dir_enabled:
        tr_sessionSetIncompleteDirEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_script_torrent_done_enabled:
        tr_sessionSetTorrentDoneScriptEnabled (tr, gtr_pref_flag_get (key));
        break;

      case TR_KEY_script_torrent_done_filename:
        tr_sessionSetTorrentDoneScript (tr, gtr_pref_string_get (key));
        break;

      case TR_KEY_start_added_torrents:
        tr_sessionSetPaused (tr, !gtr_pref_flag_get (key));
        break;

      case TR_KEY_trash_original_torrent_files:
        tr_sessionSetDeleteSource (tr, gtr_pref_flag_get (key));
        break;

      default:
        break;
    }
}

static gboolean
update_model_once (gpointer gdata)
{
  struct cbdata *data = gdata;

  /* update the torrent data in the model */
  gtr_core_update (data->core);

  /* refresh the main window's statusbar and toolbar buttons */
  if (data->wind != NULL)
    gtr_window_refresh (data->wind);

  /* update the actions */
  refresh_actions (data);

  /* update the status tray icon */
  if (data->icon != NULL)
    gtr_icon_refresh (data->icon);

  data->update_model_soon_tag = 0;
  return G_SOURCE_REMOVE;
}

static void
update_model_soon (gpointer gdata)
{
  struct cbdata *data = gdata;

  if (data->update_model_soon_tag == 0)
    data->update_model_soon_tag = gdk_threads_add_idle (update_model_once, data);
}

static gboolean
update_model_loop (gpointer gdata)
{
  const gboolean done = global_sigcount;

  if (!done)
    update_model_once (gdata);

  return !done;
}

static void
show_about_dialog (GtkWindow * parent)
{
  const char * uri = "http://www.transmissionbt.com/";
  const char * authors[] = { "Jordan Lee (Backend; GTK+)",
                             "Mitchell Livingston (Backend; OS X)",
                             NULL };

  gtk_show_about_dialog (parent,
    "authors", authors,
    "comments", _("A fast and easy BitTorrent client"),
    "copyright", _("Copyright (c) The Transmission Project"),
    "logo-icon-name", MY_CONFIG_NAME,
    "name", g_get_application_name (),
    /* Translators: translate "translator-credits" as your name
       to have it appear in the credits in the "About"
       dialog */
    "translator-credits", _("translator-credits"),
    "version", LONG_VERSION_STRING,
    "website", uri,
    "website-label", uri,
#ifdef SHOW_LICENSE
    "license", LICENSE,
    "wrap-license", TRUE,
#endif
    NULL);
}

static void
append_id_to_benc_list (GtkTreeModel * m, GtkTreePath * path UNUSED,
                        GtkTreeIter * iter, gpointer list)
{
  tr_torrent * tor = NULL;
  gtk_tree_model_get (m, iter, MC_TORRENT, &tor, -1);
  tr_variantListAddInt (list, tr_torrentId (tor));
}

static gboolean
call_rpc_for_selected_torrents (struct cbdata * data, const char * method)
{
  tr_variant top, *args, *ids;
  gboolean invoked = FALSE;
  GtkTreeSelection * s = data->sel;
  tr_session * session = gtr_core_session (data->core);

  tr_variantInitDict (&top, 2);
  tr_variantDictAddStr (&top, TR_KEY_method, method);
  args = tr_variantDictAddDict (&top, TR_KEY_arguments, 1);
  ids = tr_variantDictAddList (args, TR_KEY_ids, 0);
  gtk_tree_selection_selected_foreach (s, append_id_to_benc_list, ids);

  if (tr_variantListSize (ids) != 0)
    {
      int json_len;
      char * json = tr_variantToStr (&top, TR_VARIANT_FMT_JSON_LEAN, &json_len);
      tr_rpc_request_exec_json (session, json, json_len, NULL, NULL);
      g_free (json);
      invoked = TRUE;
    }

  tr_variantFree (&top);
  return invoked;
}

static void
open_folder_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
                     GtkTreeIter * iter, gpointer core)
{
  int id;
  gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
  gtr_core_open_folder (core, id);
}

static gboolean
on_message_window_closed (void)
{
  gtr_action_set_toggled ("toggle-message-log", FALSE);
  return FALSE;
}

static void
accumulate_selected_torrents (GtkTreeModel  * model, GtkTreePath   * path UNUSED,
                              GtkTreeIter   * iter, gpointer        gdata)
{
  int id;
  GSList ** data = gdata;

  gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
  *data = g_slist_append (*data, GINT_TO_POINTER (id));
}

static void
remove_selected (struct cbdata * data, gboolean delete_files)
{
  GSList * l = NULL;

  gtk_tree_selection_selected_foreach (data->sel, accumulate_selected_torrents, &l);

  if (l != NULL)
    gtr_confirm_remove (data->wind, data->core, l, delete_files);
}

static void
start_all_torrents (struct cbdata * data)
{
  tr_session * session = gtr_core_session (data->core);
  const char * cmd = "{ \"method\": \"torrent-start\" }";
  tr_rpc_request_exec_json (session, cmd, strlen (cmd), NULL, NULL);
}

static void
pause_all_torrents (struct cbdata * data)
{
  tr_session * session = gtr_core_session (data->core);
  const char * cmd = "{ \"method\": \"torrent-stop\" }";
  tr_rpc_request_exec_json (session, cmd, strlen (cmd), NULL, NULL);
}

static tr_torrent*
get_first_selected_torrent (struct cbdata * data)
{
  tr_torrent * tor = NULL;
  GtkTreeModel * m;
  GList * l = gtk_tree_selection_get_selected_rows (data->sel, &m);
  if (l != NULL)
    {
      GtkTreePath * p = l->data;
      GtkTreeIter i;
      if (gtk_tree_model_get_iter (m, &i, p))
        gtk_tree_model_get (m, &i, MC_TORRENT, &tor, -1);
    }
  g_list_foreach (l, (GFunc)gtk_tree_path_free, NULL);
  g_list_free (l);
  return tor;
}

static void
copy_magnet_link_to_clipboard (GtkWidget * w, tr_torrent * tor)
{
  char * magnet = tr_torrentGetMagnetLink (tor);
  GdkDisplay * display = gtk_widget_get_display (w);
  GdkAtom selection;
  GtkClipboard * clipboard;

  /* this is The Right Thing for copy/paste... */
  selection = GDK_SELECTION_CLIPBOARD;
  clipboard = gtk_clipboard_get_for_display (display, selection);
  gtk_clipboard_set_text (clipboard, magnet, -1);

  /* ...but people using plain ol' X need this instead */
  selection = GDK_SELECTION_PRIMARY;
  clipboard = gtk_clipboard_get_for_display (display, selection);
  gtk_clipboard_set_text (clipboard, magnet, -1);

  /* cleanup */
  tr_free (magnet);
}

void
gtr_actions_handler (const char * action_name, gpointer user_data)
{
  gboolean changed = FALSE;
  struct cbdata * data = user_data;

  if (!g_strcmp0 (action_name, "open-torrent-from-url"))
    {
      GtkWidget * w = gtr_torrent_open_from_url_dialog_new (data->wind, data->core);
      gtk_widget_show (w);
    }
  else if (!g_strcmp0 (action_name, "open-torrent-menu")
        || !g_strcmp0 (action_name, "open-torrent-toolbar"))
    {
      GtkWidget * w = gtr_torrent_open_from_file_dialog_new (data->wind, data->core);
      gtk_widget_show (w);
    }
    else if (!g_strcmp0 (action_name, "show-stats"))
    {
        GtkWidget * dialog = gtr_stats_dialog_new (data->wind, data->core);
        gtk_widget_show (dialog);
    }
  else if (!g_strcmp0 (action_name, "donate"))
    {
      gtr_open_uri ("http://www.transmissionbt.com/donate.php");
    }
  else if (!g_strcmp0 (action_name, "pause-all-torrents"))
    {
      pause_all_torrents (data);
    }
  else if (!g_strcmp0 (action_name, "start-all-torrents"))
    {
      start_all_torrents (data);
    }
  else if (!g_strcmp0 (action_name, "copy-magnet-link-to-clipboard"))
    {
      tr_torrent * tor = get_first_selected_torrent (data);
      if (tor != NULL)
        {
          copy_magnet_link_to_clipboard (GTK_WIDGET (data->wind), tor);
        }
    }
  else if (!g_strcmp0 (action_name, "relocate-torrent"))
    {
      GSList * ids = get_selected_torrent_ids (data);
      if (ids != NULL)
        {
          GtkWindow * parent = data->wind;
          GtkWidget * w = gtr_relocate_dialog_new (parent, data->core, ids);
          gtk_widget_show (w);
        }
    }
  else if (!g_strcmp0 (action_name, "torrent-start")
        || !g_strcmp0 (action_name, "torrent-start-now")
        || !g_strcmp0 (action_name, "torrent-stop")
        || !g_strcmp0 (action_name, "torrent-reannounce")
        || !g_strcmp0 (action_name, "torrent-verify")
        || !g_strcmp0 (action_name, "queue-move-top")
        || !g_strcmp0 (action_name, "queue-move-up")
        || !g_strcmp0 (action_name, "queue-move-down")
        || !g_strcmp0 (action_name, "queue-move-bottom"))
    {
      changed |= call_rpc_for_selected_torrents (data, action_name);
    }
  else if (!g_strcmp0 (action_name, "open-torrent-folder"))
    {
      gtk_tree_selection_selected_foreach (data->sel, open_folder_foreach, data->core);
    }
  else if (!g_strcmp0 (action_name, "show-torrent-properties"))
    {
      show_details_dialog_for_selected_torrents (data);
    }
  else if (!g_strcmp0 (action_name, "new-torrent"))
    {
      GtkWidget * w = gtr_torrent_creation_dialog_new (data->wind, data->core);
      gtk_widget_show (w);
    }
  else if (!g_strcmp0 (action_name, "remove-torrent"))
    {
      remove_selected (data, FALSE);
    }
  else if (!g_strcmp0 (action_name, "delete-torrent"))
    {
      remove_selected (data, TRUE);
    }
  else if (!g_strcmp0 (action_name, "quit"))
    {
      on_app_exit (data);
    }
  else if (!g_strcmp0 (action_name, "select-all"))
    {
      gtk_tree_selection_select_all (data->sel);
    }
  else if (!g_strcmp0 (action_name, "deselect-all"))
    {
      gtk_tree_selection_unselect_all (data->sel);
    }
  else if (!g_strcmp0 (action_name, "edit-preferences"))
    {
      if (NULL == data->prefs)
        {
          data->prefs = gtr_prefs_dialog_new (data->wind, G_OBJECT (data->core));
          g_signal_connect (data->prefs, "destroy",
                            G_CALLBACK (gtk_widget_destroyed), &data->prefs);
        }
        gtr_window_present (GTK_WINDOW (data->prefs));
    }
  else if (!g_strcmp0 (action_name, "toggle-message-log"))
    {
      if (!data->msgwin)
        {
          GtkWidget * win = gtr_message_log_window_new (data->wind, data->core);
          g_signal_connect (win, "destroy", G_CALLBACK (on_message_window_closed), NULL);
          data->msgwin = win;
        }
      else
        {
          gtr_action_set_toggled ("toggle-message-log", FALSE);
          gtk_widget_destroy (data->msgwin);
          data->msgwin = NULL;
        }
    }
  else if (!g_strcmp0 (action_name, "show-about-dialog"))
    {
      show_about_dialog (data->wind);
    }
  else if (!g_strcmp0 (action_name, "help"))
    {
      gtr_open_uri (gtr_get_help_uri ());
    }
  else if (!g_strcmp0 (action_name, "toggle-main-window"))
    {
      toggleMainWindow (data);
    }
  else if (!g_strcmp0 (action_name, "present-main-window"))
    {
      presentMainWindow (data);
    }
  else
    {
      g_error ("Unhandled action: %s", action_name);
    }

  if (changed)
    update_model_soon (data);
}
