/******************************************************************************
 * $Id$
 *
 * Copyright (c) 2005-2007 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 <sys/param.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

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

#include "conf.h"
#include "dialogs.h"
#include "ipc.h"
#include "msgwin.h"
#include "tr_backend.h"
#include "tr_cell_renderer_progress.h"
#include "tr_icon.h"
#include "tr_prefs.h"
#include "tr_torrent.h"
#include "tr_window.h"
#include "util.h"

#include "transmission.h"

#include "img_icon_full.h"

/* time in seconds to wait for torrents to stop when exiting */
#define TRACKER_EXIT_TIMEOUT    10

/* interval in milliseconds to update the torrent list display */
#define UPDATE_INTERVAL         1000

/* interval in milliseconds to check for stopped torrents and update display */
#define EXIT_CHECK_INTERVAL     500

/* number of fatal signals required to cause an immediate exit */
#define SIGCOUNT_MAX            3

struct cbdata {
    TrBackend    * back;
    GtkWindow    * wind;
    GtkTreeModel * model;
    TrIcon       * icon;
    TrPrefs      * prefs;
    guint          timer;
    gboolean       msgwinopen;
    gboolean       closing;
};

struct exitdata {
    struct cbdata * cbdata;
    time_t          started;
    guint           timer;
};

enum
{
    ACT_OPEN = 0,
    ACT_START,
    ACT_STOP,
    ACT_DELETE,
    ACT_SEPARATOR1,
    ACT_INFO,
    ACT_DEBUG,
    ACT_SEPARATOR2,
    ACT_PREF,
    ACT_SEPARATOR3,
    ACT_CLOSE,
    ACT_QUIT,
    ACT_ICON,
    ACTION_COUNT,
};

struct
{
    const char * label;
    const char * icon;
    guint        key;
    int          flags;
    const char * tooltip;
}
actions[] =
{
    { NULL,        GTK_STOCK_ADD,       'o', ACTF_WHEREVER | ACTF_ALWAYS,
      N_("Add a new torrent") },
    { N_("Start"), GTK_STOCK_EXECUTE,     0, ACTF_WHEREVER | ACTF_INACTIVE,
      N_("Start a torrent that is not running") },
    { NULL,        GTK_STOCK_STOP,        0, ACTF_WHEREVER | ACTF_ACTIVE,
      N_("Stop a torrent that is running") },
    { NULL,        GTK_STOCK_REMOVE,      0, ACTF_WHEREVER | ACTF_WHATEVER,
      N_("Remove a torrent") },
    { NULL,        NULL,                  0, ACTF_SEPARATOR, NULL },
    { NULL,        GTK_STOCK_PROPERTIES,  0, ACTF_WHEREVER | ACTF_WHATEVER,
      N_("Show additional information about a torrent") },
    { N_("Open debug window"), NULL,      0, ACTF_MENU     | ACTF_ALWAYS,
      NULL },
    { NULL,        NULL,                  0, ACTF_SEPARATOR, NULL },
    { NULL,        GTK_STOCK_PREFERENCES, 0, ACTF_WHEREVER | ACTF_ALWAYS,
      N_("Customize application behavior") },
    { NULL,        NULL,                  0, ACTF_SEPARATOR, NULL },
    { NULL,        GTK_STOCK_CLOSE,       0, ACTF_MENU     | ACTF_ALWAYS,
      N_("Close the main window") },
    { NULL,        GTK_STOCK_QUIT,        0, ACTF_MENU     | ACTF_ALWAYS,
      N_("Exit the program") },
    /* this isn't a terminator for the list, it's ACT_ICON */
    { NULL,        NULL,                  0, 0, NULL },
};

#define CBDATA_PTR              "callback-data-pointer"

static sig_atomic_t global_sigcount = 0;

static GList *
readargs( int argc, char ** argv, gboolean * sendquit, gboolean * paused );
static gboolean
sendremote( GList * files, gboolean sendquit );
static void
gtksetup( int * argc, char *** argv );
static void
appsetup( TrWindow * wind, benc_val_t * state, GList * args, gboolean paused );
static void
winsetup( struct cbdata * cbdata, TrWindow * wind );
static void
iconclick( struct cbdata * cbdata );
static void
makeicon( struct cbdata * cbdata );
static gboolean
winclose( GtkWidget * widget, GdkEvent * event, gpointer gdata );
static void
wannaquit( void * vdata );
static gboolean
exitcheck(gpointer gdata);
static void
setupdrag(GtkWidget *widget, struct cbdata *data);
static void
gotdrag(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
        GtkSelectionData *sel, guint info, guint time, gpointer gdata);

static void
readinitialprefs( struct cbdata * cbdata );
static void
prefschanged( GtkWidget * widget, int id, gpointer data );
static void
setpex( tr_torrent_t * tor, void * arg );
static gboolean
updatemodel(gpointer gdata);
static void
boolwindclosed(GtkWidget *widget, gpointer gdata);
static void
windact(GtkWidget *widget, int action, gpointer gdata);
static GList *
getselection( struct cbdata * cbdata );
static void
handleaction( struct cbdata *data, int action );

static void
addtorrents(void *vdata, void *state, GList *files,
            const char *dir, guint flags);
static void
savetorrents(struct cbdata *data);
static void
safepipe(void);
static void
setupsighandlers(void);
static void
fatalsig(int sig);

int
main( int argc, char ** argv )
{
    GtkWindow  * mainwind;
    char       * err;
    benc_val_t * state;
    GList      * argfiles;
    gboolean     didinit, didlock, sendquit, startpaused;

    safepipe();                 /* ignore SIGPIPE */
    argfiles = readargs( argc, argv, &sendquit, &startpaused );
    didinit = cf_init( tr_getPrefsDirectory(), NULL );
    didlock = FALSE;
    if( didinit )
    {
        /* maybe send remote commands, also try cf_lock() */
        didlock = sendremote( argfiles, sendquit );
    }
    setupsighandlers();         /* set up handlers for fatal signals */
    gtksetup( &argc, &argv );   /* set up gtk and gettext */

    if( ( didinit || cf_init( tr_getPrefsDirectory(), &err ) ) &&
        ( didlock || cf_lock( &err ) ) )
    {
        /* create main window now to be a parent to any error dialogs */
        mainwind = GTK_WINDOW( tr_window_new() );

        /* try to load prefs and saved state */
        cf_loadprefs( &err );
        if( NULL != err )
        {
            errmsg( mainwind, "%s", err );
            g_free( err );
        }
        state = cf_loadstate( &err );
        if( NULL != err )
        {
            errmsg( mainwind, "%s", err );
            g_free( err );
        }

        msgwin_loadpref();      /* set message level here before tr_init() */
        appsetup( TR_WINDOW( mainwind ), state, argfiles, startpaused );
        cf_freestate( state );
    }
    else
    {
        gtk_widget_show( errmsg_full( NULL, (callbackfunc_t)gtk_main_quit,
                                      NULL, "%s", err ) );
        g_free( err );
    }

    freestrlist(argfiles);

    gtk_main();

    return 0;
}

GList *
readargs( int argc, char ** argv, gboolean * sendquit, gboolean * startpaused )
{
    struct option opts[] =
    {
        { "help",    no_argument, NULL, 'h' },
        { "paused",  no_argument, NULL, 'p' },
        { "quit",    no_argument, NULL, 'q' },
        { "version", no_argument, NULL, 'v' },
        { NULL, 0, NULL, 0 }
    };
    int          opt;
    const char * name;

    *sendquit    = FALSE;
    *startpaused = FALSE;

    gtk_parse_args( &argc, &argv );
    name = g_get_prgname();

    while( 0 <= ( opt = getopt_long( argc, argv, "hpqv", opts, NULL ) ) )
    {
        switch( opt )
        {
            case 'p':
                *startpaused = TRUE;
                break;
            case 'q':
                *sendquit = TRUE;
                break;
            case 'v':
            case 'h':
                printf(
_("usage: %s [-hpq] [files...]\n"
  "\n"
  "Transmission %s (r%d) http://transmission.m0k.org/\n"
  "A free, lightweight BitTorrent client with a simple, intuitive interface\n"
  "\n"
  "  -h --help    display this message and exit\n"
  "  -p --paused  start with all torrents paused\n"
  "  -q --quit    request that the running %s instance quit\n"
  "\n"
  "Only one instance of %s may run at one time. Multiple\n"
  "torrent files may be loaded at startup by adding them to the command\n"
  "line. If %s is already running, those torrents will be\n"
  "opened in the running instance.\n"),
                        name, VERSION_STRING, VERSION_REVISION,
                        name, name, name );
                exit(0);
                break;
        }
    }

    argc -= optind;
    argv += optind;

    return checkfilenames( argc, argv );
}

static gboolean
sendremote( GList * files, gboolean sendquit )
{
    gboolean didlock;

    didlock = cf_lock( NULL );

    if( NULL != files )
    {
        /* send files if there's another instance, otherwise start normally */
        if( !didlock )
        {
            exit( ipc_sendfiles_blocking( files ) ? 0 : 1 );
        }
    }

    if( sendquit )
    {
        /* either send a quit message or exit if no other instance */
        if( !didlock )
        {
            exit( ipc_sendquit_blocking() ? 0 : 1 );
        }
        exit( 0 );
    }

    return didlock;
}

static void
gtksetup( int * argc, char *** argv )
{
    GdkPixbuf * icon;

    gtk_init( argc, argv );

    bindtextdomain( "transmission-gtk", LOCALEDIR );
    bind_textdomain_codeset( "transmission-gtk", "UTF-8" );
    textdomain( "transmission-gtk" );

    g_set_application_name( _("Transmission") );

    /* tweak some style properties in dialogs to get closer to the GNOME HiG */
    gtk_rc_parse_string(
        "style \"transmission-standard\"\n"
        "{\n"
        "    GtkDialog::action-area-border  = 6\n"
        "    GtkDialog::button-spacing      = 12\n"
        "    GtkDialog::content-area-border = 6\n"
        "}\n"
        "widget \"TransmissionDialog\" style \"transmission-standard\"\n" );

    icon = gdk_pixbuf_new_from_inline( -1, tr_icon_full, FALSE, NULL );
    gtk_window_set_default_icon( icon );
    g_object_unref( icon );
}

static void
appsetup( TrWindow * wind, benc_val_t * state, GList * args, gboolean paused )
{
    GType types[] =
    {
        /* info->name, info->totalSize, status,     error,      errorString, */
        G_TYPE_STRING, G_TYPE_UINT64,   G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING,
        /* progress,  rateDownload, rateUpload,   eta,        peersTotal, */
        G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_INT, G_TYPE_INT,
        /* peersUploading, peersDownloading, seeders,    leechers */
        G_TYPE_INT,        G_TYPE_INT,       G_TYPE_INT, G_TYPE_INT,
        /* completedFromTracker, downloaded,    uploaded       left */
        G_TYPE_INT,              G_TYPE_UINT64, G_TYPE_UINT64, G_TYPE_UINT64,
        /* tracker,            the TrTorrent object */
        TR_TRACKER_BOXED_TYPE, TR_TORRENT_TYPE,
    };
    struct cbdata * cbdata;
    GtkListStore  * store;
    guint           flags;

    /* create the model used to store torrent data */
    g_assert( ALEN( types ) == MC_ROW_COUNT );
    store = gtk_list_store_newv( MC_ROW_COUNT, types );

    /* fill out cbdata */
    cbdata = g_new0( struct cbdata, 1 );
    cbdata->back       = tr_backend_new();
    cbdata->wind       = NULL;
    cbdata->model      = GTK_TREE_MODEL(store);
    cbdata->icon       = NULL;
    cbdata->prefs      = NULL;
    cbdata->timer      = 0;
    cbdata->msgwinopen = FALSE;
    cbdata->closing    = FALSE;

    /* apply a few prefs */
    readinitialprefs( cbdata );

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

    /* add torrents from command-line and saved state */
    flags = addactionflag( tr_prefs_get( PREF_ID_ADDIPC ) );
    g_assert( !( flags & ( TR_TORNEW_PAUSED | TR_TORNEW_RUNNING ) ) );
    if( paused )
    {
        flags |= TR_TORNEW_PAUSED;
    }
    addtorrents( cbdata, state, args, NULL, flags );

    /* start model update timer */
    cbdata->timer = g_timeout_add( UPDATE_INTERVAL, updatemodel, cbdata );
    updatemodel( cbdata );

    /* set up the ipc socket now that we're ready to get torrents from it */
    ipc_socket_setup( GTK_WINDOW( wind ), addtorrents, wannaquit, cbdata );

    /* show the window */
    tr_window_show( wind );
}

static void
winsetup( struct cbdata * cbdata, TrWindow * wind )
{
    int ii;
    GtkWidget  * drag;

    g_assert( ACTION_COUNT == ALEN( actions ) );
    g_assert( NULL == cbdata->wind );
    cbdata->wind = GTK_WINDOW( wind );
    for( ii = 0; ii < ALEN( actions ); ii++ )
    {
        tr_window_action_add( wind, ii, actions[ii].flags,
                              gettext( actions[ii].label ), actions[ii].icon,
                              gettext( actions[ii].tooltip ),
                              actions[ii].key );
    }
    g_object_set( wind, "model", cbdata->model,
                        "double-click-action", ACT_INFO, NULL);

    g_signal_connect( wind, "action",       G_CALLBACK( windact  ), cbdata );
    g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata );
    
    g_object_get( wind, "drag-widget", &drag, NULL );
    setupdrag( drag, cbdata );
}

static void
iconclick( struct cbdata * cbdata )
{
    GtkWidget * win;

    if( NULL != cbdata->wind )
    {
        /* close window  */
        winclose( NULL, NULL, cbdata );
    }
    else
    {
        /* create window */
        win = tr_window_new();
        winsetup( cbdata, TR_WINDOW( win ) );
        tr_window_show( TR_WINDOW( win ) );
    }
}

static void
makeicon( struct cbdata * cbdata )
{
    TrIcon * icon;
    int      ii;

    if( NULL != cbdata->icon )
    {
        return;
    }

    icon = tr_icon_new();
    for( ii = 0; ii < ALEN( actions ); ii++ )
    {
        tr_icon_action_add( TR_ICON( icon ), ii, actions[ii].flags,
                              gettext( actions[ii].label ), actions[ii].icon );
    }
    g_object_set( icon, "activate-action", ACT_ICON, NULL);
    g_signal_connect( icon, "action", G_CALLBACK( windact ), cbdata );

    cbdata->icon = icon;
}

static gboolean
winclose( GtkWidget * widget SHUTUP, GdkEvent * event SHUTUP, gpointer gdata )
{
    struct cbdata * cbdata;

    cbdata = gdata;

    if( NULL != cbdata->icon && tr_icon_docked( cbdata->icon ) )
    {
        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
        cbdata->wind = NULL;
    }
    else
    {
        askquit( cbdata->wind, wannaquit, cbdata );
    }

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

static void
wannaquit( void * vdata )
{
  struct cbdata * data;
  struct exitdata *edata;
  GtkTreeIter iter;
  TrTorrent *tor;

  data = vdata;
  if( data->closing )
  {
      return;
  }
  data->closing = TRUE;

  /* stop the update timer */
  if(0 < data->timer)
    g_source_remove(data->timer);
  data->timer = 0;

  /*
    Add a reference to all torrents in the list, which will be removed
    when the politely-stopped signal is emitted.  This is necessary
    because a reference is added when a torrent is removed
    from the model and tr_torrent_stop_polite() is called on it.
  */
  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
    do
      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
    while(gtk_tree_model_iter_next(data->model, &iter));
  }

  /* try to politely stop all the torrents */
  tr_backend_stop_torrents(data->back);

  /* shut down nat traversal */
  tr_natTraversalEnable(tr_backend_handle(data->back), 0);

  /* set things up to wait for torrents to stop */
  edata = g_new0(struct exitdata, 1);
  edata->cbdata = data;
  edata->started = time(NULL);
  /* check if torrents are still running */
  if(exitcheck(edata)) {
    /* yes, start the exit timer and disable widgets */
    edata->timer = g_timeout_add(EXIT_CHECK_INTERVAL, exitcheck, edata);
    if( NULL != data->wind )
    {
        gtk_widget_set_sensitive( GTK_WIDGET( data->wind ), FALSE );
    }
  }
}

static gboolean
exitcheck( gpointer gdata )
{
    struct exitdata    * edata;
    struct cbdata      * cbdata;
    tr_handle_status_t * hstat;

    edata  = gdata;
    cbdata = edata->cbdata;
    hstat  = tr_handleStatus( tr_backend_handle( cbdata->back ) );

    /* keep going if we haven't hit the exit timeout and
       we either have torrents left or nat traversal is active */
    if( time( NULL ) - edata->started < TRACKER_EXIT_TIMEOUT )
    {
        if( !tr_backend_torrents_stopped( cbdata->back, FALSE ) ||
            TR_NAT_TRAVERSAL_DISABLED != hstat->natTraversalStatus )
        {
            updatemodel( cbdata );
            return TRUE;
        }
    }
    else
    {
        /* time the remaining torrents out so they signal politely-stopped */
        tr_backend_torrents_stopped( cbdata->back, TRUE );
    }

    /* exit otherwise */
    if( 0 < edata->timer )
    {
        g_source_remove( edata->timer );
    }
    g_free( edata );
    /* The prefs window need to be destroyed first as destroying it may
       trigger callbacks that use cbdata->back. Ick. */
    if( NULL != cbdata->prefs )
    {
        gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) );
    }
    g_object_unref( G_OBJECT( cbdata->back ) );
    if( NULL != cbdata->wind )
    {
        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
    }
    g_object_unref( cbdata->model );
    if( NULL != cbdata->icon )
    {
        g_object_unref( cbdata->icon );
    }
    g_assert( 0 == cbdata->timer );
    g_free( cbdata );
    gtk_main_quit();

    return FALSE;
}

static void
gotdrag(GtkWidget *widget SHUTUP, GdkDragContext *dc, gint x SHUTUP,
        gint y SHUTUP, GtkSelectionData *sel, guint info SHUTUP, guint time,
        gpointer gdata) {
  struct cbdata *data = gdata;
  char prefix[] = "file:";
  char *files, *decoded, *deslashed, *hostless;
  int ii, len;
  GList *errs;
  struct stat sb;
  int prelen = strlen(prefix);
  GList *paths, *freeables;

#ifdef DND_DEBUG
  char *sele = gdk_atom_name(sel->selection);
  char *targ = gdk_atom_name(sel->target);
  char *type = gdk_atom_name(sel->type);

  fprintf(stderr, "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i\n",
          sele, targ, type, sel->format, sel->length);
  g_free(sele);
  g_free(targ);
  g_free(type);
  if(8 == sel->format) {
    for(ii = 0; ii < sel->length; ii++)
      fprintf(stderr, "%02X ", sel->data[ii]);
    fprintf(stderr, "\n");
  }
#endif

  errs = NULL;
  paths = NULL;
  freeables = NULL;
  if(gdk_atom_intern("XdndSelection", FALSE) == sel->selection &&
     8 == sel->format) {
    /* split file list on carriage returns and linefeeds */
    files = g_new(char, sel->length + 1);
    memcpy(files, sel->data, sel->length);
    files[sel->length] = '\0';
    for(ii = 0; '\0' != files[ii]; ii++)
      if('\015' == files[ii] || '\012' == files[ii])
        files[ii] = '\0';

    /* try to get a usable filename out of the URI supplied and add it */
    for(ii = 0; ii < sel->length; ii += len + 1) {
      if('\0' == files[ii])
        len = 0;
      else {
        len = strlen(files + ii);
        /* de-urlencode the URI */
        decoded = urldecode(files + ii, len);
        freeables = g_list_append(freeables, decoded);
        if(g_utf8_validate(decoded, -1, NULL)) {
          /* remove the file: prefix */
          if(prelen < len && 0 == strncmp(prefix, decoded, prelen)) {
            deslashed = decoded + prelen;
            /* trim excess / characters from the beginning */
            while('/' == deslashed[0] && '/' == deslashed[1])
              deslashed++;
            /* if the file doesn't exist, the first part might be a hostname */
            if(0 > g_stat(deslashed, &sb) &&
               NULL != (hostless = strchr(deslashed + 1, '/')) &&
               0 == g_stat(hostless, &sb))
              deslashed = hostless;
            /* finally, add it to the list of torrents to try adding */
            paths = g_list_append(paths, deslashed);
          }
        }
      }
    }

    /* try to add any torrents we found */
    if( NULL != paths )
    {
        addtorrents( data, NULL, paths, NULL,
                     addactionflag( tr_prefs_get( PREF_ID_ADDSTD ) ) );
    }
    freestrlist(freeables);
    g_free(files);
  }

  gtk_drag_finish(dc, (NULL != paths), FALSE, time);
}

static void
setupdrag(GtkWidget *widget, struct cbdata *data) {
  GtkTargetEntry targets[] = {
    { "STRING",     0, 0 },
    { "text/plain", 0, 0 },
    { "text/uri-list", 0, 0 },
  };

  g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data);

  gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets,
                    ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE);
}

static void
readinitialprefs( struct cbdata * cbdata )
{
    int prefs[] =
    {
        PREF_ID_PORT,
        PREF_ID_USEDOWNLIMIT,
        PREF_ID_USEUPLIMIT,
        PREF_ID_NAT,
        PREF_ID_ICON,
        PREF_ID_PEX,
    };
    int ii;

    for( ii = 0; ALEN( prefs ) > ii; ii++ )
    {
        prefschanged( NULL, prefs[ii], cbdata );
    }
}

static void
prefschanged( GtkWidget * widget SHUTUP, int id, gpointer data )
{
    struct cbdata * cbdata;
    tr_handle_t   * tr;
    int             num;
    gboolean        boolval;

    cbdata = data;
    tr     = tr_backend_handle( cbdata->back );

    switch( id )
    {
        case PREF_ID_PORT:
            tr_setBindPort( tr, tr_prefs_get_int_with_default( id ) );
            break;

        case PREF_ID_USEDOWNLIMIT:
        case PREF_ID_DOWNLIMIT:
            num = -1;
            if( tr_prefs_get_bool_with_default( PREF_ID_USEDOWNLIMIT ) )
            {
                num = tr_prefs_get_int_with_default( PREF_ID_DOWNLIMIT );
            }
            tr_setGlobalDownloadLimit( tr, num );
            break;

        case PREF_ID_USEUPLIMIT:
        case PREF_ID_UPLIMIT:
            num = -1;
            if( tr_prefs_get_bool_with_default( PREF_ID_USEUPLIMIT ) )
            {
                num = tr_prefs_get_int_with_default( PREF_ID_UPLIMIT );
            }
            tr_setGlobalUploadLimit( tr, num );
            break;

        case PREF_ID_NAT:
            tr_natTraversalEnable( tr, tr_prefs_get_bool_with_default( id ) );
            break;

        case PREF_ID_ICON:
            if( tr_prefs_get_bool_with_default( id ) )
            {
                makeicon( cbdata );
            }
            else if( NULL != cbdata->icon )
            {
                g_object_unref( cbdata->icon );
                cbdata->icon = NULL;
            }
            break;

        case PREF_ID_PEX:
            boolval = tr_prefs_get_bool_with_default( id );
            tr_torrentIterate( tr, setpex, &boolval );
            break;

        case PREF_ID_DIR:
        case PREF_ID_ASKDIR:
        case PREF_ID_ADDSTD:
        case PREF_ID_ADDIPC:
        case PREF_ID_MSGLEVEL:
        case PREF_MAX_ID:
            break;
    }
}

void
setpex( tr_torrent_t * tor, void * arg )
{
    gboolean * val;

    val = arg;
    tr_torrentDisablePex( tor, !(*val) );
}

gboolean
updatemodel(gpointer gdata) {
  struct cbdata *data = gdata;
  TrTorrent *tor;
  tr_stat_t *st;
  GtkTreeIter iter;
  float up, down;

  if( !data->closing && 0 < global_sigcount )
  {
      wannaquit( data );
      return FALSE;
  }

  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
    do {
      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
      st = tr_torrent_stat(tor);
      g_object_unref(tor);
      /* XXX find out if setting the same data emits changed signal */
      gtk_list_store_set( GTK_LIST_STORE( data->model ), &iter,
          MC_STAT,   st->status,               MC_ERR,     st->error,
          MC_TERR,   st->errorString,          MC_PROG,    st->progress,
          MC_DRATE,  st->rateDownload,         MC_URATE,   st->rateUpload,
          MC_ETA,    st->eta,                  MC_PEERS,   st->peersTotal,
          MC_UPEERS, st->peersUploading,       MC_DPEERS,  st->peersDownloading,
          MC_SEED,   st->seeders,              MC_LEECH,   st->leechers,
          MC_DONE,   st->completedFromTracker, MC_TRACKER, st->tracker,
          MC_DOWN,   st->downloaded,           MC_UP,      st->uploaded,
          MC_LEFT,   st->left, -1);
    } while(gtk_tree_model_iter_next(data->model, &iter));
  }

  /* update the main window's statusbar and toolbar buttons */
  if( NULL != data->wind )
  {
      tr_torrentRates( tr_backend_handle( data->back ), &down, &up );
      tr_window_update( TR_WINDOW( data->wind ), down, up );
  }

  /* check for politely stopped torrents unless we're exiting */
  if( !data->closing )
  {
      tr_backend_torrents_stopped( data->back, FALSE );
  }

  /* update the message window */
  msgwin_update();

  return TRUE;
}

static void
boolwindclosed(GtkWidget *widget SHUTUP, gpointer gdata) {
  gboolean *preachy_gcc = gdata;
  
  *preachy_gcc = FALSE;
}

static void
windact( GtkWidget * wind SHUTUP, int action, gpointer gdata )
{
    g_assert( 0 <= action );
    handleaction( gdata, action );
}

/* returns a GList containing a GtkTreeRowReference to each selected row */
static GList *
getselection( struct cbdata * cbdata )
{
    GtkTreeSelection    * sel;
    GList               * rows, * ii;
    GtkTreeRowReference * ref;

    if( NULL == cbdata->wind )
    {
        return NULL;
    }
    g_object_get( cbdata->wind, "selection", &sel, NULL );
    rows = gtk_tree_selection_get_selected_rows( sel, NULL );
    for( ii = rows; NULL != ii; ii = ii->next )
    {
        ref = gtk_tree_row_reference_new( cbdata->model, ii->data );
        gtk_tree_path_free( ii->data );
        ii->data = ref;
    }

    return rows;
}

static void
handleaction( struct cbdata * data, int act )
{
  GList *rows, *ii;
  GtkTreePath *path;
  GtkTreeIter iter;
  TrTorrent *tor;
  int status;
  gboolean changed;
  GtkWidget * win;

  g_assert( ACTION_COUNT > act );

  switch( act )
  {
      case ACT_OPEN:
          makeaddwind( data->wind, addtorrents, data );
          return;
      case ACT_PREF:
          if( NULL != data->prefs )
          {
              return;
          }
          data->prefs = tr_prefs_new_with_parent( data->wind );
          g_signal_connect( data->prefs, "prefs-changed",
                            G_CALLBACK( prefschanged ), data );
          g_signal_connect( data->prefs, "destroy",
                            G_CALLBACK( gtk_widget_destroyed ), &data->prefs );
          gtk_widget_show( GTK_WIDGET( data->prefs ) );
          return;
      case ACT_DEBUG:
          if( !data->msgwinopen )
          {
              data->msgwinopen = TRUE;
              win = msgwin_create();
              g_signal_connect( win, "destroy", G_CALLBACK( boolwindclosed ),
                                &data->msgwinopen );
          }
          return;
      case ACT_ICON:
          iconclick( data );
          return;
      case ACT_CLOSE:
          if( NULL != data->wind )
          {
              winclose( NULL, NULL, data );
          }
          return;
      case ACT_QUIT:
          askquit( data->wind, wannaquit, data );
          return;
      case ACT_SEPARATOR1:
      case ACT_SEPARATOR2:
      case ACT_SEPARATOR3:
          return;
      case ACT_START:
      case ACT_STOP:
      case ACT_DELETE:
      case ACT_INFO:
      case ACTION_COUNT:
          break;
  }

  /* get a list of references to selected rows */
  rows = getselection( data );

  changed = FALSE;
  for(ii = rows; NULL != ii; ii = ii->next) {
    if(NULL != (path = gtk_tree_row_reference_get_path(ii->data)) &&
       gtk_tree_model_get_iter(data->model, &iter, path)) {
      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
                         MC_STAT, &status, -1);
      if( ACT_ISAVAIL( actions[act].flags, status ) )

      {
          switch( act )
          {
              case ACT_START:
                  tr_torrentStart( tr_torrent_handle( tor ) );
                  changed = TRUE;
                  break;
              case ACT_STOP:
                  tr_torrentStop( tr_torrent_handle( tor ) );
                  changed = TRUE;
                  break;
              case ACT_DELETE:
                  /* tor will be unref'd in the politely_stopped handler */
                  g_object_ref( tor );
                  tr_torrent_stop_politely( tor );
                  if( TR_FLAG_SAVE & tr_torrent_info( tor )->flags )
                  {
                      tr_torrentRemoveSaved( tr_torrent_handle( tor ) );
                  }
                  gtk_list_store_remove( GTK_LIST_STORE( data->model ),
                                         &iter );
                  changed = TRUE;
                  break;
              case ACT_INFO:
                  makeinfowind( data->wind, data->model, path, tor );
                  break;
              case ACT_OPEN:
              case ACT_PREF:
              case ACT_DEBUG:
              case ACT_ICON:
              case ACT_CLOSE:
              case ACT_QUIT:
              case ACT_SEPARATOR1:
              case ACT_SEPARATOR2:
              case ACT_SEPARATOR3:
              case ACTION_COUNT:
                  break;
          }
      }
      g_object_unref(tor);
    }
    if(NULL != path)
      gtk_tree_path_free(path);
    gtk_tree_row_reference_free(ii->data);
  }
  g_list_free(rows);

  if(changed) {
    savetorrents(data);
    updatemodel(data);
  }
}

static void
addtorrents(void *vdata, void *state, GList *files,
            const char *dir, guint flags) {
  struct cbdata *data = vdata;
  GList *torlist, *errlist, *ii;
  char *errstr;
  TrTorrent *tor;
  GtkTreeIter iter;
  const char * pref;
  tr_info_t *in;

  errlist = NULL;
  torlist = NULL;

  if( NULL != state )
  {
      torlist = tr_backend_load_state( data->back, state, flags, &errlist );
  }

  if(NULL != files) {
    if( NULL == dir )
    {
        pref = tr_prefs_get( PREF_ID_ASKDIR );
        if( NULL != pref && strbool( pref ) )
        {
            promptfordir( data->wind, addtorrents, data, files, flags );
            files = NULL;
        }
        dir = getdownloaddir();
    }
    for(ii = g_list_first(files); NULL != ii; ii = ii->next) {
      errstr = NULL;
      tor = tr_torrent_new(G_OBJECT(data->back), ii->data, dir,
                           flags, &errstr);
      if(NULL != tor)
        torlist = g_list_append(torlist, tor);
      if(NULL != errstr)
        errlist = g_list_append(errlist, errstr);
    }
  }

  for( ii = g_list_first( torlist ); NULL != ii; ii = ii->next )
  {
      gtk_list_store_append( GTK_LIST_STORE( data->model ), &iter );
      in = tr_torrent_info( ii->data );
      gtk_list_store_set( GTK_LIST_STORE( data->model ), &iter,
                          MC_NAME,    in->name,
                          MC_SIZE,    in->totalSize,
                          MC_TORRENT, ii->data, -1);
      /* we will always ref a torrent before politely stopping it */
      g_signal_connect( ii->data, "politely_stopped",
                        G_CALLBACK( g_object_unref ), data );
      g_object_unref( ii->data );
  }

  if(NULL != errlist) {
    errstr = joinstrlist(errlist, "\n");
    errmsg( data->wind, ngettext( "Failed to load torrent file:\n%s",
                                  "Failed to load torrent files:\n%s",
                                  g_list_length( errlist ) ), errstr );
    g_list_foreach(errlist, (GFunc)g_free, NULL);
    g_list_free(errlist);
    g_free(errstr);
  }

  if(NULL != torlist) {
    updatemodel(data);
    savetorrents(data);
  }
}

static void
savetorrents( struct cbdata *data )
{
    char * errstr;

    tr_backend_save_state( data->back, &errstr );
    if( NULL != errstr )
    {
        errmsg( data->wind, "%s", errstr );
        g_free( errstr );
    }
}

static void
safepipe(void) {
  struct sigaction sa;

  bzero(&sa, sizeof(sa));
  sa.sa_handler = SIG_IGN;
  sigaction(SIGPIPE, &sa, NULL);
}

static void
setupsighandlers(void) {
  int sigs[] = {SIGHUP, SIGINT, SIGQUIT, SIGTERM};
  struct sigaction sa;
  int ii;

  bzero(&sa, sizeof(sa));
  sa.sa_handler = fatalsig;
  for(ii = 0; ii < ALEN(sigs); ii++)
    sigaction(sigs[ii], &sa, NULL);
}

static void
fatalsig(int sig) {
  struct sigaction sa;

  if(SIGCOUNT_MAX <= ++global_sigcount) {
    bzero(&sa, sizeof(sa));
    sa.sa_handler = SIG_DFL;
    sigaction(sig, &sa, NULL);
    raise(sig);
  }
}
