/******************************************************************************
 * $Id: remote.c 4908 2008-02-03 07:03:39Z charles $
 *
 * Copyright (c) 2007 Joshua Elsasser
 *
 * 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/types.h>
#include <sys/param.h>
#include <sys/time.h>
#include <assert.h>
#include <ctype.h>
#include <event.h>
#include <getopt.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <libtransmission/bsdtree.h>
#include <libtransmission/ipcparse.h>
#include <libtransmission/transmission.h>
#include <libtransmission/trcompat.h>

#include "bsdqueue.h"
#include "client.h"
#include "errors.h"
#include "misc.h"

#define BESTDECIMAL(d)          (10.0 > (d) ? 2 : (100.0 > (d) ? 1 : 0))

struct opts
{
    int               proxy;
    char           ** proxycmd;
    enum confpathtype type;
    const char      * sock;
    struct strlist    files;
    int               sendquit;
    int               port;
    int               map;
    int               uplimit;
    int               up;
    int               downlimit;
    int               down;
    int               listquick;
    int               listfull;
    int               startall;
    struct strlist    start;
    int               stopall;
    struct strlist    stop;
    int               removeall;
    struct strlist    remove;
    char              dir[MAXPATHLEN];
    int               pex;
    const char *      crypto;
};

struct torinfo
{
    int     id;
    int     infogood;
    int     statgood;
    char  * name;
    int64_t size;
    char  * state;
    int64_t eta;
    int64_t done;
    int64_t ratedown;
    int64_t rateup;
    int64_t totaldown;
    int64_t totalup;
    char  * errorcode;
    char  * errormsg;
    RB_ENTRY( torinfo ) idlinks;
    RB_ENTRY( torinfo ) namelinks;
};

RB_HEAD( torlist, torinfo );
RB_HEAD( tornames, torinfo );

struct torhash
{
    char                hash[SHA_DIGEST_LENGTH*2+1];
    int                 id;
    RB_ENTRY( torhash ) link;
};

RB_HEAD( torhashes, torhash );

static void   usage        ( const char *, ... );
static int    readargs     ( int, char **, struct opts * );
static int    numarg       ( const char * );
static int    hasharg      ( const char *, struct strlist *, int * );
static int    fileargs     ( struct strlist *, int, char * const * );
static void   listmsg      ( const struct cl_info * );
static void   infomsg      ( const struct cl_info * );
static void   statmsg      ( const struct cl_stat * );
static void   hashmsg      ( const struct cl_info * );
static float  fmtsize      ( int64_t, const char ** );
static char * strdup_noctrl( const char * );
static void   print_eta    ( int64_t );
static void   printlisting ( void );
static int    sendidreqs   ( void );
static int    toridcmp     ( struct torinfo *, struct torinfo * );
static int    tornamecmp   ( struct torinfo *, struct torinfo * );
static int    torhashcmp   ( struct torhash *, struct torhash * );

static struct torlist   gl_torinfo      = RB_INITIALIZER( &gl_torinfo );
static struct strlist * gl_starthashes  = NULL;
static struct strlist * gl_stophashes   = NULL;
static struct strlist * gl_removehashes = NULL;
static struct torhashes gl_hashids      = RB_INITIALIZER( &gl_hashids );
static int              gl_gotlistinfo  = 0;
static int              gl_gotliststat  = 0;

RB_GENERATE_STATIC( torlist,   torinfo, idlinks,   toridcmp )
RB_GENERATE_STATIC( tornames,  torinfo, namelinks, tornamecmp )
RB_GENERATE_STATIC( torhashes, torhash, link,      torhashcmp )

int
main( int argc, char ** argv )
{
    struct event_base * evbase;
    struct opts         o;
    char                sockpath[MAXPATHLEN];

    setmyname( argv[0] );
    if( 0 > readargs( argc, argv, &o ) )
    {
        exit( 1 );
    }

    signal( SIGPIPE, SIG_IGN );

    evbase = event_init();
    client_init( evbase );

    if( o.proxy )
    {
        client_new_cmd( o.proxycmd );
    }
    else
    {
        if( NULL == o.sock )
        {
            confpath( sockpath, sizeof sockpath, CONF_FILE_SOCKET, o.type );
            client_new_sock( sockpath );
        }
        else
        {
            client_new_sock( o.sock );
        }
    }

    if( ( o.sendquit                &&   0 > client_quit     (           ) ) ||
        ( '\0' != o.dir[0]          &&   0 > client_dir      ( o.dir     ) ) ||
        ( !SLIST_EMPTY( &o.files )  &&   0 > client_addfiles ( &o.files  ) ) ||
        ( o.crypto                  &&   0 > client_crypto   ( o.crypto  ) ) ||
        ( o.startall                &&   0 > client_start    ( 0, NULL   ) ) ||
        ( o.stopall                 &&   0 > client_stop     ( 0, NULL   ) ) ||
        ( o.removeall               &&   0 > client_remove   ( 0, NULL   ) ) ||
        ( o.port                    &&   0 > client_port     ( o.port    ) ) ||
        ( 0 <= o.map                &&   0 > client_automap  ( o.map     ) ) ||
        ( 0 <= o.pex                &&   0 > client_pex      ( o.pex     ) ) ||
        ( o.uplimit                 &&   0 > client_uplimit  ( o.up      ) ) ||
        ( o.downlimit               &&   0 > client_downlimit( o.down    ) ) ||
        ( o.listquick               &&   0 > client_list     ( listmsg   ) ) ||
        ( o.listfull                && ( 0 > client_info     ( infomsg     ) ||
                                         0 > client_status   ( statmsg ) ) ) )
    {
        exit( 1 );
    }

    if( ( !o.startall  && !SLIST_EMPTY( &o.start  ) ) ||
        ( !o.stopall   && !SLIST_EMPTY( &o.stop   ) ) ||
        ( !o.removeall && !SLIST_EMPTY( &o.remove ) ) )
    {
        if( 0 > client_hashids( hashmsg ) )
        {
            exit( 1 );
        }
        gl_starthashes  = ( o.startall  ? NULL : &o.start  );
        gl_stophashes   = ( o.stopall   ? NULL : &o.stop   );
        gl_removehashes = ( o.removeall ? NULL : &o.remove );
    }

    event_dispatch();
    /* event_base_dispatch( evbase ); */

    return 1;
}

void
usage( const char * msg, ... )
{
    va_list ap;

    if( NULL != msg )
    {
        printf( "%s: ", getmyname() );
        va_start( ap, msg );
        vprintf( msg, ap );
        va_end( ap );
        printf( "\n" );
    }

    printf(
  "usage: %s [options] [files...]\n"
  "       %s -x [options] proxy-command [args...]\n"
  "\n"
  "Transmission %s http://www.transmissionbt.com/\n"
  "A fast and easy BitTorrent client\n"
  "\n"
  "  -a --add <torrent>        Add a torrent\n"
  "  -c --encryption preferred Prefer peers to use encryption\n"
  "  -c --encryption required  Require encryption for all peers\n"
  "  -d --download-limit <int> Max download rate in KiB/s\n"
  "  -D --download-unlimited   No download rate limit\n"
  "  -e --enable-pex           Enable peer exchange\n"
  "  -E --disable-pex          Disable peer exchange\n"
  "  -f --folder <path>        Folder to set for new torrents\n"
  "  -h --help                 Display this message and exit\n"
  "  -i --info                 List all torrents with info hashes\n"
  "  -l --list                 List all torrents with status\n"
  "  -m --port-mapping         Automatic port mapping via NAT-PMP or UPnP\n"
  "  -M --no-port-mapping      Disable automatic port mapping\n"
  "  -p --port <int>           Port to listen for incoming connections on\n"
  "  -q --quit                 Quit the daemon\n"
  "  -r --remove <hash>        Remove the torrent with the given hash\n"
  "  -r --remove all           Remove all torrents\n"
  "  -s --start <hash>         Start the torrent with the given hash\n"
  "  -s --start all            Start all stopped torrents\n"
  "  -S --stop <hash>          Stop the torrent with the given hash\n"
  "  -S --stop all             Stop all running torrents\n"
  "  -t --type daemon          Use the daemon frontend, transmission-daemon\n"
  "  -t --type gtk             Use the GTK+ frontend, transmission\n"
  "  -t --type mac             Use the MacOS X frontend\n"
  "  -u --upload-limit <int>   Max upload rate in KiB/s\n"
  "  -U --upload-unlimited     No upload rate limit\n"
  "  -x --proxy                Use proxy command to connect to frontend\n",
            getmyname(), getmyname(), LONG_VERSION_STRING );
    exit( 0 );
}

int
readargs( int argc, char ** argv, struct opts * opts )
{
    char optstr[] = "a:c:d:DeEf:hilmMp:qr:s:S:t:u:Ux";
    struct option longopts[] =
    {
        { "add",                required_argument, NULL, 'a' },
        { "encryption",         required_argument, NULL, 'c' },
        { "download-limit",     required_argument, NULL, 'd' },
        { "download-unlimited", no_argument,       NULL, 'D' },
        { "enable-pex",         no_argument,       NULL, 'e' },
        { "disable-pex",        no_argument,       NULL, 'E' },
        { "folder",             required_argument, NULL, 'f' },
        { "help",               no_argument,       NULL, 'h' },
        { "info",               no_argument,       NULL, 'i' },
        { "list",               no_argument,       NULL, 'l' },
        { "port-mapping",       no_argument,       NULL, 'm' },
        { "no-port-mapping",    no_argument,       NULL, 'M' },
        { "port",               required_argument, NULL, 'p' },
        { "quit",               no_argument,       NULL, 'q' },
        { "remove",             required_argument, NULL, 'r' },
        { "start",              required_argument, NULL, 's' },
        { "stop",               required_argument, NULL, 'S' },
        { "type",               required_argument, NULL, 't' },
        { "upload-limit",       required_argument, NULL, 'u' },
        { "upload-unlimited",   no_argument,       NULL, 'U' },
        { "proxy",              no_argument,       NULL, 'x' },
        { NULL, 0, NULL, 0 }
    };
    int opt, gotmsg;

    gotmsg = 0;
    memset( opts, 0, sizeof *opts );
    opts->type = CONF_PATH_TYPE_DAEMON;
    SLIST_INIT( &opts->files );
    opts->map = -1;
    opts->pex = -1;
    SLIST_INIT( &opts->start );
    SLIST_INIT( &opts->stop );
    SLIST_INIT( &opts->remove );

    while( 0 <= ( opt = getopt_long( argc, argv, optstr, longopts, NULL ) ) )
    {
        switch( opt )
        {
            case 'a':
                if( 0 > fileargs( &opts->files, 1, &optarg ) )
                {
                    return -1;
                }
                break;
            case 'c':
                if(!strcasecmp(optarg, "preferred"))
                    opts->crypto = "preferred";
                else if(!strcasecmp(optarg, "required"))
                    opts->crypto = "required";
                else
                    usage("invalid encryption mode: %s", optarg);
                break;
            case 'd':
                opts->downlimit = 1;
                opts->down      = numarg( optarg );
                break;
            case 'D':
                opts->downlimit = 1;
                opts->down      = -1;
                break;
            case 'e':
                opts->pex       = 1;
                break;
            case 'E':
                opts->pex       = 0;
                break;
            case 'f':
                absolutify( opts->dir, sizeof opts->dir, optarg );
                break;
            case 'i':
                opts->listquick = 1;
                break;
            case 'l':
                opts->listfull  = 1;
                break;
            case 'm':
                opts->map       = 1;
                break;
            case 'M':
                opts->map       = 0;
                break;
            case 'p':
                opts->port      = numarg( optarg );
                if( 0 >= opts->port || 0xffff <= opts->port )
                {
                    usage( "invalid port: %i", opts->port );
                }
                break;
            case 'q':
                opts->sendquit  = 1;
                break;
            case 'r':
                if( 0 > hasharg( optarg, &opts->remove, &opts->removeall ) )
                {
                    return -1;
                }
                break;
            case 's':
                if( 0 > hasharg( optarg, &opts->start, &opts->startall ) )
                {
                    return -1;
                }
                break;
            case 'S':
                if( 0 > hasharg( optarg, &opts->stop, &opts->stopall ) )
                {
                    return -1;
                }
                break;
            case 't':
                if( 0 == strcasecmp( "daemon", optarg ) )
                {
                    opts->type  = CONF_PATH_TYPE_DAEMON;
                    opts->sock  = NULL;
                }
                else if( 0 == strcasecmp( "gtk", optarg ) )
                {
                    opts->type  = CONF_PATH_TYPE_GTK;
                    opts->sock  = NULL;
                }
                else if( 0 == strcasecmp( "mac", optarg ) ||
                         0 == strcasecmp( "osx", optarg ) ||
                         0 == strcasecmp( "macos", optarg ) ||
                         0 == strcasecmp( "macosx", optarg ) )
                {
                    opts->type  = CONF_PATH_TYPE_OSX;
                    opts->sock  = NULL;
                }
                else
                {
                    opts->sock  = optarg;
                }
                break;
            case 'u':
                opts->uplimit   = 1;
                opts->up        = numarg( optarg );
                break;
            case 'U':
                opts->uplimit   = 1;
                opts->up        = -1;
                break;
            case 'x':
                opts->proxy     = 1;
                continue; /* don't set gotmsg, -x isn't a message */
            default:
                usage( NULL );
                break;
        }
        gotmsg = 1;
    }

    if( !gotmsg && argc == optind )
    {
        usage( NULL );
    }

    if( opts->proxy )
    {
        if( argc == optind )
            usage( "can't use -x without any arguments" );
        opts->proxycmd = argv + optind;
    }
    else if( 0 > fileargs( &opts->files, argc - optind, argv + optind ) )
    {
        return -1;
    }

    return 0;
}

int
numarg( const char * arg )
{
    char * end;
    long   num;

    end = NULL;
    num = strtol( arg, &end, 10 );
    if( NULL != end && '\0' != *end )
    {
        usage( "not a number: %s", arg );
        return -1;
    }

    return num;
}

int
hasharg( const char * arg, struct strlist * list, int * all )
{
    struct stritem * listitem;
    struct torhash * treeitem, key, * foo;
    size_t           len, ii;

    /* check for special "all" value */
    if( 0 == strcasecmp( "all", arg ) )
    {
        *all = 1;
        return 0;
    }

    /* check hash length */
    len = strlen( arg );
    if( SHA_DIGEST_LENGTH * 2 != len )
    {
        usage( "torrent info hash length is not %i: %s",
               SHA_DIGEST_LENGTH * 2, arg );
        return -1;
    }

    /* allocate list item */
    listitem = calloc( 1, sizeof *listitem );
    if( NULL == listitem )
    {
        mallocmsg( sizeof *listitem );
        return -1;
    }
    listitem->str = calloc( len + 1, 1 );
    if( NULL == listitem->str )
    {
        mallocmsg( len + 1 );
        free( listitem );
        return -1;
    }

    /* check that the hash is all hex and copy it in lowercase */
    for( ii = 0; len > ii; ii++ )
    {
        if( !isxdigit( arg[ii] ) )
        {
            usage( "torrent info hash is not hex: %s", arg );
            free( listitem->str );
            free( listitem );
            return -1;
        }
        listitem->str[ii] = tolower( arg[ii] );
    }

    /* try to look up the hash in the hash tree */
    memset( &key, 0, sizeof key );
    strlcpy( key.hash, listitem->str, sizeof key.hash );
    treeitem = RB_FIND( torhashes, &gl_hashids, &key );
    if( NULL == treeitem )
    {
        /* the hash isn't in the tree, allocate a tree item and insert it */
        treeitem = calloc( 1, sizeof *treeitem );
        if( NULL == treeitem )
        {
            mallocmsg( sizeof *treeitem );
            free( listitem->str );
            free( listitem );
            return -1;
        }
        treeitem->id = -1;
        strlcpy( treeitem->hash, listitem->str, sizeof treeitem->hash );
        foo = RB_INSERT( torhashes, &gl_hashids, treeitem );
        assert( NULL == foo );
    }

    /* finally, add the list item to the list */
    SLIST_INSERT_HEAD( list, listitem, next );

    return 0;
}

int
fileargs( struct strlist * list, int argc, char * const * argv )
{
    struct stritem * item;
    int              ii;
    char             path[MAXPATHLEN];

    for( ii = 0; argc > ii; ii++ )
    {
        item = calloc( 1, sizeof *item );
        if( NULL == item )
        {
            mallocmsg( sizeof *item );
            return -1;
        }
        if( '/' == argv[ii][0] )
        {
            item->str = strdup( argv[ii] );
            if( NULL == item->str )
            {
                mallocmsg( strlen( argv[ii] ) + 1 );
                free( item );
                return -1;
            }
        }
        else
        {
            absolutify( path, sizeof path, argv[ii] );
            item->str = strdup( path );
            if( NULL == item->str )
            {
                mallocmsg( strlen( path ) + 1 );
                free( item );
                return -1;
            }
        }
        SLIST_INSERT_HEAD( list, item, next );
    }

    return 0;
}

void
listmsg( const struct cl_info * inf )
{
    char * newname;
    size_t ii;

    if( NULL == inf )
    {
        return;
    }

    if( NULL == inf->name )
    {
        errmsg( "missing torrent name from server" );
        return;
    }
    else if( NULL == inf->hash )
    {
        errmsg( "missing torrent hash from server" );
        return;
    }

    newname = strdup_noctrl( inf->name );
    if( NULL == newname )
    {
        return;
    }

    for( ii = 0; '\0' != inf->hash[ii]; ii++ )
    {
        if( isxdigit( inf->hash[ii] ) )
        {
            putchar( tolower( inf->hash[ii] ) );
        }
    }
    putchar( ' ' );
    printf( "%s\n", newname );
    free( newname );
}

void
infomsg( const struct cl_info * cinfo )
{
    struct torinfo * tinfo, key;

    gl_gotlistinfo = 1;

    if( NULL == cinfo )
    {
        if( gl_gotliststat )
        {
            printlisting();
        }
        return;
    }

    if( NULL == cinfo->name || 0 >= cinfo->size )
    {
        errmsg( "missing torrent info from server" );
        return;
    }

    memset( &key, 0, sizeof key );
    key.id = cinfo->id;
    tinfo = RB_FIND( torlist, &gl_torinfo, &key );
    if( NULL == tinfo )
    {
        tinfo = calloc( 1, sizeof *tinfo );
        if( NULL == tinfo )
        {
            mallocmsg( sizeof *tinfo );
            return;
        }
        tinfo->id = cinfo->id;
        RB_INSERT( torlist, &gl_torinfo, tinfo );
    }

    tinfo->infogood = 1;
    free( tinfo->name );
    tinfo->name = strdup_noctrl( cinfo->name );
    tinfo->size = cinfo->size;
}

void
statmsg( const struct cl_stat * st )
{
    struct torinfo * info, key;

    gl_gotliststat = 1;

    if( NULL == st )
    {
        if( gl_gotlistinfo )
        {
            printlisting();
        }
        return;
    }

    if( NULL == st->state || 0 > st->done ||
        0 > st->ratedown  || 0 > st->rateup ||
        0 > st->totaldown || 0 > st->totalup )
    {
        errmsg( "missing torrent status from server" );
        return;
    }

    memset( &key, 0, sizeof key );
    key.id = st->id;
    info = RB_FIND( torlist, &gl_torinfo, &key );
    if( NULL == info )
    {
        info = calloc( 1, sizeof *info );
        if( NULL == info )
        {
            mallocmsg( sizeof *info );
            return;
        }
        info->id = st->id;
        RB_INSERT( torlist, &gl_torinfo, info );
    }

    info->statgood = 1;
    free( info->state );
    info->state     = strdup_noctrl( st->state );
    info->eta       = st->eta;
    info->done      = st->done;
    info->ratedown  = st->ratedown;
    info->rateup    = st->rateup;
    info->totaldown = st->totaldown;
    info->totalup   = st->totalup;
    if( NULL != st->error && '\0' != st->error[0] )
    {
        info->errorcode = strdup_noctrl( st->error );
    }
    if( NULL != st->errmsg && '\0' != st->errmsg[0] )
    {
        info->errormsg = strdup_noctrl( st->errmsg );
    }
}

void
hashmsg( const struct cl_info * inf )
{
    struct torhash key, * found;

    if( NULL == inf )
    {
        sendidreqs();
        return;
    }

    if( NULL == inf->hash )
    {
        errmsg( "missing torrent hash from server" );
        return;
    }

    memset( &key, 0, sizeof key );
    strlcpy( key.hash, inf->hash, sizeof key.hash );
    found = RB_FIND( torhashes, &gl_hashids, &key );
    if( NULL != found )
    {
        found->id = inf->id;
    }
}

float
fmtsize( int64_t num, const char ** units )
{
    static const char * sizes[] =
    {
        "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB",
    };
    float  ret;
    size_t ii;

    ret = num;
    for( ii = 0; ARRAYLEN( sizes ) > ii && 1000.0 < ret; ii++ )
    {
        ret /= 1024.0;
    }

    if( NULL != units )
    {
        *units = sizes[ii];
    }

    return ret;
}

char *
strdup_noctrl( const char * str )
{
    char * ret;
    size_t ii;

    ii = strlen( str );
    ret = malloc( ii + 1 );
    if( NULL == ret )
    {
        mallocmsg( ii + 1 );
        return NULL;
    }

    for( ii = 0; '\0' != str[ii]; ii++ )
    {
        ret[ii] = ( iscntrl( str[ii] ) ? ' ' : str[ii] );
    }
    ret[ii] = '\0';

    return ret;
}

void
print_eta( int64_t secs )
{
    static const struct
    {
        const char * label;
        int64_t      div;
    }
    units[] =
    {
        { "second", 60 },
        { "minute", 60 },
        { "hour",   24 },
        { "day",    7  },
        { "week",   1 }
    };
    int    readable[ ARRAYLEN( units ) ];
    int    printed;
    size_t ii;

    if( 0 > secs )
    {
        printf( "stalled" );
        return;
    }

    for( ii = 0; ARRAYLEN( units ) > ii; ii++ )
    {
        readable[ii] = secs % units[ii].div;
        secs /= units[ii].div;
    }
    readable[ii-1] = MIN( INT_MAX, secs );

    printf( "done in" );
    for( printed = 0; 0 < ii && 2 > printed; ii-- )
    {
        if( 0 != readable[ii-1] ||
            ( 0 == printed && 1 == ii ) )
        {
            printf( " %i %s%s", readable[ii-1], units[ii-1].label,
                    ( 1 == readable[ii-1] ? "" : "s" ) );
            printed++;
        }
    }
}

int
sendidreqs( void )
{
    struct
    {
        struct strlist * list;
        int ( * func )( size_t, const int * );
    }
    reqs[] =
    {
        { gl_starthashes,  client_start  },
        { gl_stophashes,   client_stop   },
        { gl_removehashes, client_remove },
    };
    struct stritem * jj;
    size_t           ii;
    int            * ids, count, ret;
    struct torhash   key, * found;

    ret = -1;
    memset( &key, 0, sizeof key );

    for( ii = 0; ARRAYLEN( reqs ) > ii; ii++)
    {
        if( NULL == reqs[ii].list || SLIST_EMPTY( reqs[ii].list ) )
        {
            continue;
        }

        count = 0;
        SLIST_FOREACH( jj, reqs[ii].list, next )
        {
            count++;
        }
        count++;

        ids = calloc( count, sizeof ids[0] );
        if( NULL == ids )
        {
            mallocmsg( count * sizeof ids[0] );
            return -1;
        }

        count = 0;
        SLIST_FOREACH( jj, reqs[ii].list, next )
        {
            strlcpy( key.hash, jj->str, sizeof key.hash );
            found = RB_FIND( torhashes, &gl_hashids, &key );
            if( NULL != found && TORRENT_ID_VALID( found->id ) )
            {
                ids[count] = found->id;
                count++;
            }
        }

        if( 0 < count )
        {
            if( 0 > reqs[ii].func( count, ids ) )
            {
                free( ids );
                return -1;
            }
            ret = 0;
        }

        free( ids );
    }

    gl_starthashes  = NULL;
    gl_stophashes   = NULL;
    gl_removehashes = NULL;

    return ret;
}

void
printlisting( void )
{
    struct tornames  names;
    struct torinfo * ii, * next;
    const char     * units, * name, * upunits, * downunits;
    float            size, progress, upspeed, downspeed, ratio;

    /* sort the torrents by name */
    RB_INIT( &names );
    RB_FOREACH( ii, torlist, &gl_torinfo )
    {
        RB_INSERT( tornames, &names, ii );
    }

    /* print the torrent info while freeing the tree */
    for( ii = RB_MIN( tornames, &names ); NULL != ii; ii = next )
    {
        next = RB_NEXT( tornames, &names, ii );
        RB_REMOVE( tornames, &names, ii );
        RB_REMOVE( torlist, &gl_torinfo, ii );

        if( !ii->infogood || !ii->statgood )
        {
            goto free;
        }

        /* massage some numbers into a better format for printing */
        size      = fmtsize( ii->size, &units );
        upspeed   = fmtsize( ii->rateup, &upunits );
        downspeed = fmtsize( ii->ratedown, &downunits );
        name      = ( NULL == ii->name ? "???" : ii->name );
        progress  = ( float )ii->done / ( float )ii->size * 100.0;
        progress  = MIN( 100.0, progress );
        progress  = MAX( 0.0, progress );

        /* print name and size */
        printf( "%s (%.*f %s) - ", name, BESTDECIMAL( size ), size, units );

        /* print hash check progress */
        if( 0 == strcasecmp( "checking", ii->state ) )
        {
            printf( "%.*f%% checking files",
                    BESTDECIMAL( progress ), progress );
        }
        /* print progress */
        else if( 0 == strcasecmp( "waiting to checking", ii->state ) )
        {
            printf( "%.*f%% waiting to check files",
                    BESTDECIMAL( progress ), progress );
        }
        /* print download progress, speeds, and eta */
        else if( 0 == strcasecmp( "downloading", ii->state ) )
        {
            progress = MIN( 99.0, progress );
            printf( "%.*f%% downloading at %.*f %s/s (UL at %.*f %s/s), ",
                    BESTDECIMAL( progress ), progress,
                    BESTDECIMAL( downspeed ), downspeed, downunits,
                    BESTDECIMAL( upspeed ), upspeed, upunits );
            print_eta( ii->eta );
        }
        /* print seeding speed */
        else if( 0 == strcasecmp( "seeding", ii->state ) )
        {
            if( 0 == ii->totalup && 0 == ii->totaldown )
            {
                printf( "100%% seeding at %.*f %s/s [N/A]",
                        BESTDECIMAL( upspeed ), upspeed, upunits );
            }
            else if( 0 == ii->totaldown )
            {
                printf( "100%% seeding at %.*f %s/s [INF]",
                        BESTDECIMAL( upspeed ), upspeed, upunits );
            }
            else
            {
                ratio = ( float )ii->totalup / ( float )ii->totaldown;
                printf( "100%% seeding at %.*f %s/s [%.*f]",
                        BESTDECIMAL( upspeed ), upspeed, upunits,
                        BESTDECIMAL( ratio ), ratio );
            }
        }
        /* print stopping message */
        else if( 0 == strcasecmp( "stopping", ii->state ) )
        {
            printf( "%.*f%% stopping...", BESTDECIMAL( progress ), progress );
        }
        /* print stopped message with progress */
        else if( 0 == strcasecmp( "paused", ii->state ) )
        {
            printf( "%.*f%% stopped", BESTDECIMAL( progress ), progress );
        }
        /* unknown status, just print it with progress */
        else
        {
            printf( "%.*f%% %s",
                    BESTDECIMAL( progress ), progress, ii->state );
        }

        /* print any error */
        if( NULL != ii->errorcode || NULL != ii->errormsg )
        {
            if( NULL == ii->errorcode )
            {
                printf( " [error - %s]", ii->errormsg );
            }
            else if( NULL == ii->errormsg )
            {
                printf( " [error (%s)]", ii->errorcode );
            }
            else
            {
                printf( " [error (%s) - %s]", ii->errorcode, ii->errormsg );
            }
        }

        /* don't forget the newline */
        putchar( '\n' );

        /* free the stuff for this torrent */
      free:
        free( ii->name );
        free( ii->state );
        free( ii->errorcode );
        free( ii->errormsg );
        free( ii );
    }
}

int
tornamecmp( struct torinfo * left, struct torinfo * right )
{
    int ret;

    /* we know they're equal if they're the same struct */
    if( left == right )
    {
        return 0;
    }
    /* we might be missing a name, fall back on torrent ID */
    else if( NULL == left->name && NULL == right->name )
    {
        return toridcmp( left, right );
    }
    else if( NULL == left->name )
    {
        return -1;
    }
    else if( NULL == right->name )
    {
        return 1;
    }

    /* if we have two names, compare them */
    ret = strcasecmp( left->name, right->name );
    if( 0 != ret )
    {
        return ret;
    }
    /* if the names are the same then fall back on the torrent ID,
       this is to handle different torrents with the same name */
    else
    {
        return toridcmp( left, right );
    }
}

int
torhashcmp( struct torhash * left, struct torhash * right )
{
    return strcasecmp( left->hash, right->hash );
}

INTCMP_FUNC( toridcmp, torinfo, id )
