/*
 * This file Copyright (C) 2008-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 */

#include <assert.h>
#include <ctype.h> /* isspace */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> /* strcmp */

#include <event2/buffer.h>
#include <event2/util.h>

#define CURL_DISABLE_TYPECHECK /* otherwise -Wunreachable-code goes insane */
#include <curl/curl.h>

#include <libtransmission/transmission.h>
#include <libtransmission/crypto-utils.h>
#include <libtransmission/error.h>
#include <libtransmission/file.h>
#include <libtransmission/log.h>
#include <libtransmission/rpcimpl.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/utils.h>
#include <libtransmission/variant.h>
#include <libtransmission/version.h>

#define MY_NAME "transmission-remote"
#define DEFAULT_HOST "localhost"
#define DEFAULT_PORT atoi(TR_DEFAULT_RPC_PORT_STR)
#define DEFAULT_URL TR_DEFAULT_RPC_URL_STR "rpc/"

#define ARGUMENTS TR_KEY_arguments

#define MEM_K 1024
#define MEM_B_STR "B"
#define MEM_K_STR "KiB"
#define MEM_M_STR "MiB"
#define MEM_G_STR "GiB"
#define MEM_T_STR "TiB"

#define DISK_K 1000
#define DISK_B_STR "B"
#define DISK_K_STR "kB"
#define DISK_M_STR "MB"
#define DISK_G_STR "GB"
#define DISK_T_STR "TB"

#define SPEED_K 1000
#define SPEED_B_STR "B/s"
#define SPEED_K_STR "kB/s"
#define SPEED_M_STR "MB/s"
#define SPEED_G_STR "GB/s"
#define SPEED_T_STR "TB/s"

/***
****
****  Display Utilities
****
***/

static void etaToString(char* buf, size_t buflen, int64_t eta)
{
    if (eta < 0)
    {
        tr_snprintf(buf, buflen, "Unknown");
    }
    else if (eta < 60)
    {
        tr_snprintf(buf, buflen, "%" PRId64 " sec", eta);
    }
    else if (eta < (60 * 60))
    {
        tr_snprintf(buf, buflen, "%" PRId64 " min", eta / 60);
    }
    else if (eta < (60 * 60 * 24))
    {
        tr_snprintf(buf, buflen, "%" PRId64 " hrs", eta / (60 * 60));
    }
    else
    {
        tr_snprintf(buf, buflen, "%" PRId64 " days", eta / (60 * 60 * 24));
    }
}

static char* tr_strltime(char* buf, int seconds, size_t buflen)
{
    int days;
    int hours;
    int minutes;
    int total_seconds;
    char b[128];
    char d[128];
    char h[128];
    char m[128];
    char s[128];
    char t[128];

    if (seconds < 0)
    {
        seconds = 0;
    }

    total_seconds = seconds;
    days = seconds / 86400;
    hours = (seconds % 86400) / 3600;
    minutes = (seconds % 3600) / 60;
    seconds = (seconds % 3600) % 60;

    tr_snprintf(d, sizeof(d), "%d %s", days, days == 1 ? "day" : "days");
    tr_snprintf(h, sizeof(h), "%d %s", hours, hours == 1 ? "hour" : "hours");
    tr_snprintf(m, sizeof(m), "%d %s", minutes, minutes == 1 ? "minute" : "minutes");
    tr_snprintf(s, sizeof(s), "%d %s", seconds, seconds == 1 ? "second" : "seconds");
    tr_snprintf(t, sizeof(t), "%d %s", total_seconds, total_seconds == 1 ? "second" : "seconds");

    if (days != 0)
    {
        if (days >= 4 || hours == 0)
        {
            tr_strlcpy(b, d, sizeof(b));
        }
        else
        {
            tr_snprintf(b, sizeof(b), "%s, %s", d, h);
        }
    }
    else if (hours != 0)
    {
        if (hours >= 4 || minutes == 0)
        {
            tr_strlcpy(b, h, sizeof(b));
        }
        else
        {
            tr_snprintf(b, sizeof(b), "%s, %s", h, m);
        }
    }
    else if (minutes != 0)
    {
        if (minutes >= 4 || seconds == 0)
        {
            tr_strlcpy(b, m, sizeof(b));
        }
        else
        {
            tr_snprintf(b, sizeof(b), "%s, %s", m, s);
        }
    }
    else
    {
        tr_strlcpy(b, s, sizeof(b));
    }

    tr_snprintf(buf, buflen, "%s (%s)", b, t);
    return buf;
}

static char* strlpercent(char* buf, double x, size_t buflen)
{
    return tr_strpercent(buf, x, buflen);
}

static char* strlratio2(char* buf, double ratio, size_t buflen)
{
    return tr_strratio(buf, buflen, ratio, "Inf");
}

static char* strlratio(char* buf, int64_t numerator, int64_t denominator, size_t buflen)
{
    double ratio;

    if (denominator != 0)
    {
        ratio = numerator / (double)denominator;
    }
    else if (numerator != 0)
    {
        ratio = TR_RATIO_INF;
    }
    else
    {
        ratio = TR_RATIO_NA;
    }

    return strlratio2(buf, ratio, buflen);
}

static char* strlmem(char* buf, int64_t bytes, size_t buflen)
{
    if (bytes == 0)
    {
        tr_strlcpy(buf, "None", buflen);
    }
    else
    {
        tr_formatter_mem_B(buf, bytes, buflen);
    }

    return buf;
}

static char* strlsize(char* buf, int64_t bytes, size_t buflen)
{
    if (bytes < 0)
    {
        tr_strlcpy(buf, "Unknown", buflen);
    }
    else if (bytes == 0)
    {
        tr_strlcpy(buf, "None", buflen);
    }
    else
    {
        tr_formatter_size_B(buf, bytes, buflen);
    }

    return buf;
}

enum
{
    TAG_SESSION,
    TAG_STATS,
    TAG_DETAILS,
    TAG_FILES,
    TAG_LIST,
    TAG_PEERS,
    TAG_PIECES,
    TAG_PORTTEST,
    TAG_TORRENT_ADD,
    TAG_TRACKERS
};

static char const* getUsage(void)
{
    return MY_NAME " " LONG_VERSION_STRING "\n"
        "A fast and easy BitTorrent client\n"
        "https://transmissionbt.com/\n"
        "\n"
        "Usage: " MY_NAME " [host] [options]\n"
        "       " MY_NAME " [port] [options]\n"
        "       " MY_NAME " [host:port] [options]\n"
        "       " MY_NAME " [http(s?)://host:port/transmission/] [options]\n"
        "\n"
        "See the man page for detailed explanations and many examples.";
}

/***
****
****  Command-Line Arguments
****
***/

static tr_option opts[] =
{
    { 'a', "add", "Add torrent files by filename or URL", "a", false, NULL },
    { 970, "alt-speed", "Use the alternate Limits", "as", false, NULL },
    { 971, "no-alt-speed", "Don't use the alternate Limits", "AS", false, NULL },
    { 972, "alt-speed-downlimit", "max alternate download speed (in "SPEED_K_STR ")", "asd", true, "<speed>" },
    { 973, "alt-speed-uplimit", "max alternate upload speed (in "SPEED_K_STR ")", "asu", true, "<speed>" },
    { 974, "alt-speed-scheduler", "Use the scheduled on/off times", "asc", false, NULL },
    { 975, "no-alt-speed-scheduler", "Don't use the scheduled on/off times", "ASC", false, NULL },
    { 976, "alt-speed-time-begin", "Time to start using the alt speed limits (in hhmm)", NULL, true, "<time>" },
    { 977, "alt-speed-time-end", "Time to stop using the alt speed limits (in hhmm)", NULL, true, "<time>" },
    { 978, "alt-speed-days", "Numbers for any/all days of the week - eg. \"1-7\"", NULL, true, "<days>" },
    { 963, "blocklist-update", "Blocklist update", NULL, false, NULL },
    { 'c', "incomplete-dir", "Where to store new torrents until they're complete", "c", true, "<dir>" },
    { 'C', "no-incomplete-dir", "Don't store incomplete torrents in a different location", "C", false, NULL },
    { 'b', "debug", "Print debugging information", "b", false, NULL },
    { 'd', "downlimit", "Set the max download speed in "SPEED_K_STR " for the current torrent(s) or globally", "d", true,
        "<speed>" },
    { 'D', "no-downlimit", "Disable max download speed for the current torrent(s) or globally", "D", false, NULL },
    { 'e', "cache", "Set the maximum size of the session's memory cache (in " MEM_M_STR ")", "e", true, "<size>" },
    { 910, "encryption-required", "Encrypt all peer connections", "er", false, NULL },
    { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", false, NULL },
    { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", false, NULL },
    { 850, "exit", "Tell the transmission session to shut down", NULL, false, NULL },
    { 940, "files", "List the current torrent(s)' files", "f", false, NULL },
    { 'g', "get", "Mark files for download", "g", true, "<files>" },
    { 'G', "no-get", "Mark files for not downloading", "G", true, "<files>" },
    { 'i', "info", "Show the current torrent(s)' details", "i", false, NULL },
    { 940, "info-files", "List the current torrent(s)' files", "if", false, NULL },
    { 941, "info-peers", "List the current torrent(s)' peers", "ip", false, NULL },
    { 942, "info-pieces", "List the current torrent(s)' pieces", "ic", false, NULL },
    { 943, "info-trackers", "List the current torrent(s)' trackers", "it", false, NULL },
    { 920, "session-info", "Show the session's details", "si", false, NULL },
    { 921, "session-stats", "Show the session's statistics", "st", false, NULL },
    { 'l', "list", "List all torrents", "l", false, NULL },
    { 'L', "labels", "Set the current torrents' labels", "L", true, "<label[,label...]>" },
    { 960, "move", "Move current torrent's data to a new folder", NULL, true, "<path>" },
    { 961, "find", "Tell Transmission where to find a torrent's data", NULL, true, "<path>" },
    { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, NULL },
    { 'M', "no-portmap", "Disable portmapping", "M", false, NULL },
    { 'n', "auth", "Set username and password", "n", true, "<user:pw>" },
    { 810, "authenv", "Set authentication info from the TR_AUTH environment variable (user:pw)", "ne", false, NULL },
    { 'N', "netrc", "Set authentication info from a .netrc file", "N", true, "<file>" },
    { 820, "ssl", "Use SSL when talking to daemon", NULL, false, NULL },
    { 'o', "dht", "Enable distributed hash tables (DHT)", "o", false, NULL },
    { 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", false, NULL },
    { 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", true, "<port>" },
    { 962, "port-test", "Port testing", "pt", false, NULL },
    { 'P', "random-port", "Random port for incoming peers", "P", false, NULL },
    { 900, "priority-high", "Try to download these file(s) first", "ph", true, "<files>" },
    { 901, "priority-normal", "Try to download these file(s) normally", "pn", true, "<files>" },
    { 902, "priority-low", "Try to download these file(s) last", "pl", true, "<files>" },
    { 700, "bandwidth-high", "Give this torrent first chance at available bandwidth", "Bh", false, NULL },
    { 701, "bandwidth-normal", "Give this torrent bandwidth left over by high priority torrents", "Bn", false, NULL },
    { 702, "bandwidth-low", "Give this torrent bandwidth left over by high and normal priority torrents", "Bl", false, NULL },
    { 600, "reannounce", "Reannounce the current torrent(s)", NULL, false, NULL },
    { 'r', "remove", "Remove the current torrent(s)", "r", false, NULL },
    { 930, "peers", "Set the maximum number of peers for the current torrent(s) or globally", "pr", true, "<max>" },
    { 840, "remove-and-delete", "Remove the current torrent(s) and delete local data", "rad", false, NULL },
    { 800, "torrent-done-script", "Specify a script to run when a torrent finishes", NULL, true, "<file>" },
    { 801, "no-torrent-done-script", "Don't run a script when torrents finish", NULL, false, NULL },
    { 950, "seedratio", "Let the current torrent(s) seed until a specific ratio", "sr", true, "ratio" },
    { 951, "seedratio-default", "Let the current torrent(s) use the global seedratio settings", "srd", false, NULL },
    { 952, "no-seedratio", "Let the current torrent(s) seed regardless of ratio", "SR", false, NULL },
    { 953, "global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio",
        "gsr", true, "ratio" },
    { 954, "no-global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio",
        "GSR", false, NULL },
    { 710, "tracker-add", "Add a tracker to a torrent", "td", true, "<tracker>" },
    { 712, "tracker-remove", "Remove a tracker from a torrent", "tr", true, "<trackerId>" },
    { 's', "start", "Start the current torrent(s)", "s", false, NULL },
    { 'S', "stop", "Stop the current torrent(s)", "S", false, NULL },
    { 't', "torrent", "Set the current torrent(s)", "t", true, "<torrent>" },
    { 990, "start-paused", "Start added torrents paused", NULL, false, NULL },
    { 991, "no-start-paused", "Start added torrents unpaused", NULL, false, NULL },
    { 992, "trash-torrent", "Delete torrents after adding", NULL, false, NULL },
    { 993, "no-trash-torrent", "Do not delete torrents after adding", NULL, false, NULL },
    { 984, "honor-session", "Make the current torrent(s) honor the session limits", "hl", false, NULL },
    { 985, "no-honor-session", "Make the current torrent(s) not honor the session limits", "HL", false, NULL },
    { 'u', "uplimit", "Set the max upload speed in "SPEED_K_STR " for the current torrent(s) or globally", "u", true,
        "<speed>" },
    { 'U', "no-uplimit", "Disable max upload speed for the current torrent(s) or globally", "U", false, NULL },
    { 830, "utp", "Enable uTP for peer connections", NULL, false, NULL },
    { 831, "no-utp", "Disable uTP for peer connections", NULL, false, NULL },
    { 'v', "verify", "Verify the current torrent(s)", "v", false, NULL },
    { 'V', "version", "Show version number and exit", "V", false, NULL },
    { 'w', "download-dir", "When used in conjunction with --add, set the new torrent's download folder. "
        "Otherwise, set the default download folder", "w", true, "<path>" },
    { 'x', "pex", "Enable peer exchange (PEX)", "x", false, NULL },
    { 'X', "no-pex", "Disable peer exchange (PEX)", "X", false, NULL },
    { 'y', "lpd", "Enable local peer discovery (LPD)", "y", false, NULL },
    { 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", false, NULL },
    { 941, "peer-info", "List the current torrent(s)' peers", "pi", false, NULL },
    { 0, NULL, NULL, NULL, false, NULL }
};

static void showUsage(void)
{
    tr_getopt_usage(MY_NAME, getUsage(), opts);
}

static int numarg(char const* arg)
{
    char* end = NULL;
    long const num = strtol(arg, &end, 10);

    if (*end != '\0')
    {
        fprintf(stderr, "Not a number: \"%s\"\n", arg);
        showUsage();
        exit(EXIT_FAILURE);
    }

    return num;
}

enum
{
    MODE_TORRENT_START = (1 << 0),
    MODE_TORRENT_STOP = (1 << 1),
    MODE_TORRENT_VERIFY = (1 << 2),
    MODE_TORRENT_REANNOUNCE = (1 << 3),
    MODE_TORRENT_SET = (1 << 4),
    MODE_TORRENT_GET = (1 << 5),
    MODE_TORRENT_ADD = (1 << 6),
    MODE_TORRENT_REMOVE = (1 << 7),
    MODE_TORRENT_SET_LOCATION = (1 << 8),
    MODE_SESSION_SET = (1 << 9),
    MODE_SESSION_GET = (1 << 10),
    MODE_SESSION_STATS = (1 << 11),
    MODE_SESSION_CLOSE = (1 << 12),
    MODE_BLOCKLIST_UPDATE = (1 << 13),
    MODE_PORT_TEST = (1 << 14)
};

static int getOptMode(int val)
{
    switch (val)
    {
    case TR_OPT_ERR:
    case TR_OPT_UNK:
    case 'a': /* add torrent */
    case 'b': /* debug */
    case 'n': /* auth */
    case 810: /* authenv */
    case 'N': /* netrc */
    case 820: /* UseSSL */
    case 't': /* set current torrent */
    case 'V': /* show version number */
        return 0;

    case 'c': /* incomplete-dir */
    case 'C': /* no-incomplete-dir */
    case 'e': /* cache */
    case 'm': /* portmap */
    case 'M': /* "no-portmap */
    case 'o': /* dht */
    case 'O': /* no-dht */
    case 'p': /* incoming peer port */
    case 'P': /* random incoming peer port */
    case 'x': /* pex */
    case 'X': /* no-pex */
    case 'y': /* lpd */
    case 'Y': /* no-lpd */
    case 800: /* torrent-done-script */
    case 801: /* no-torrent-done-script */
    case 830: /* utp */
    case 831: /* no-utp */
    case 970: /* alt-speed */
    case 971: /* no-alt-speed */
    case 972: /* alt-speed-downlimit */
    case 973: /* alt-speed-uplimit */
    case 974: /* alt-speed-scheduler */
    case 975: /* no-alt-speed-scheduler */
    case 976: /* alt-speed-time-begin */
    case 977: /* alt-speed-time-end */
    case 978: /* alt-speed-days */
    case 910: /* encryption-required */
    case 911: /* encryption-preferred */
    case 912: /* encryption-tolerated */
    case 953: /* global-seedratio */
    case 954: /* no-global-seedratio */
    case 990: /* start-paused */
    case 991: /* no-start-paused */
    case 992: /* trash-torrent */
    case 993: /* no-trash-torrent */
        return MODE_SESSION_SET;

    case 'L': /* labels */
    case 712: /* tracker-remove */
    case 950: /* seedratio */
    case 951: /* seedratio-default */
    case 952: /* no-seedratio */
    case 984: /* honor-session */
    case 985: /* no-honor-session */
        return MODE_TORRENT_SET;

    case 920: /* session-info */
        return MODE_SESSION_GET;

    case 'g': /* get */
    case 'G': /* no-get */
    case 700: /* torrent priority-high */
    case 701: /* torrent priority-normal */
    case 702: /* torrent priority-low */
    case 710: /* tracker-add */
    case 900: /* file priority-high */
    case 901: /* file priority-normal */
    case 902: /* file priority-low */
        return MODE_TORRENT_SET | MODE_TORRENT_ADD;

    case 961: /* find */
        return MODE_TORRENT_SET_LOCATION | MODE_TORRENT_ADD;

    case 'i': /* info */
    case 'l': /* list all torrents */
    case 940: /* info-files */
    case 941: /* info-peer */
    case 942: /* info-pieces */
    case 943: /* info-tracker */
        return MODE_TORRENT_GET;

    case 'd': /* download speed limit */
    case 'D': /* no download speed limit */
    case 'u': /* upload speed limit */
    case 'U': /* no upload speed limit */
    case 930: /* peers */
        return MODE_SESSION_SET | MODE_TORRENT_SET;

    case 's': /* start */
        return MODE_TORRENT_START | MODE_TORRENT_ADD;

    case 'S': /* stop */
        return MODE_TORRENT_STOP | MODE_TORRENT_ADD;

    case 'w': /* download-dir */
        return MODE_SESSION_SET | MODE_TORRENT_ADD;

    case 850: /* session-close */
        return MODE_SESSION_CLOSE;

    case 963: /* blocklist-update */
        return MODE_BLOCKLIST_UPDATE;

    case 921: /* session-stats */
        return MODE_SESSION_STATS;

    case 'v': /* verify */
        return MODE_TORRENT_VERIFY;

    case 600: /* reannounce */
        return MODE_TORRENT_REANNOUNCE;

    case 962: /* port-test */
        return MODE_PORT_TEST;

    case 'r': /* remove */
    case 840: /* remove and delete */
        return MODE_TORRENT_REMOVE;

    case 960: /* move */
        return MODE_TORRENT_SET_LOCATION;

    default:
        fprintf(stderr, "unrecognized argument %d\n", val);
        assert("unrecognized argument" && 0);
        return 0;
    }
}

static bool debug = false;
static char* auth = NULL;
static char* netrc = NULL;
static char* sessionId = NULL;
static bool UseSSL = false;

static char* getEncodedMetainfo(char const* filename)
{
    size_t len = 0;
    char* b64 = NULL;
    uint8_t* buf = tr_loadFile(filename, &len, NULL);

    if (buf != NULL)
    {
        b64 = tr_base64_encode(buf, len, NULL);
        tr_free(buf);
    }

    return b64;
}

static void addIdArg(tr_variant* args, char const* id, char const* fallback)
{
    if (tr_str_is_empty(id))
    {
        id = fallback;

        if (tr_str_is_empty(id))
        {
            fprintf(stderr, "No torrent specified!  Please use the -t option first.\n");
            id = "-1"; /* no torrent will have this ID, so will act as a no-op */
        }
    }

    if (tr_strcmp0(id, "active") == 0)
    {
        tr_variantDictAddStr(args, TR_KEY_ids, "recently-active");
    }
    else if (strcmp(id, "all") != 0)
    {
        bool isList = strchr(id, ',') != NULL || strchr(id, '-') != NULL;
        bool isNum = true;

        for (char const* pch = id; isNum && *pch != '\0'; ++pch)
        {
            isNum = isdigit(*pch);
        }

        if (isNum || isList)
        {
            tr_rpc_parse_list_str(tr_variantDictAdd(args, TR_KEY_ids), id, strlen(id));
        }
        else
        {
            tr_variantDictAddStr(args, TR_KEY_ids, id); /* it's a torrent sha hash */
        }
    }
}

static void addTime(tr_variant* args, tr_quark const key, char const* arg)
{
    int time;
    bool success = false;

    if (arg != NULL && strlen(arg) == 4)
    {
        char const hh[3] = { arg[0], arg[1], '\0' };
        char const mm[3] = { arg[2], arg[3], '\0' };
        int const hour = atoi(hh);
        int const min = atoi(mm);

        if (0 <= hour && hour < 24 && 0 <= min && min < 60)
        {
            time = min + (hour * 60);
            success = true;
        }
    }

    if (success)
    {
        tr_variantDictAddInt(args, key, time);
    }
    else
    {
        fprintf(stderr, "Please specify the time of day in 'hhmm' format.\n");
    }
}

static void addDays(tr_variant* args, tr_quark const key, char const* arg)
{
    int days = 0;

    if (arg != NULL)
    {
        int valueCount;
        int* values;

        values = tr_parseNumberRange(arg, TR_BAD_SIZE, &valueCount);

        for (int i = 0; i < valueCount; ++i)
        {
            if (values[i] < 0 || values[i] > 7)
            {
                continue;
            }

            if (values[i] == 7)
            {
                values[i] = 0;
            }

            days |= 1 << values[i];
        }

        tr_free(values);
    }

    if (days != 0)
    {
        tr_variantDictAddInt(args, key, days);
    }
    else
    {
        fprintf(stderr, "Please specify the days of the week in '1-3,4,7' format.\n");
    }
}

static void addLabels(tr_variant* args, char const* arg)
{
    tr_variant* labels;
    if (!tr_variantDictFindList(args, TR_KEY_labels, &labels))
    {
        labels = tr_variantDictAddList(args, TR_KEY_labels, 10);
    }

    char* argcpy = tr_strdup(arg);
    char* const tmp = argcpy; /* save copied string start pointer to free later */
    char* token;
    while ((token = tr_strsep(&argcpy, ",")) != NULL)
    {
        tr_strstrip(token);
        if (!tr_str_is_empty(token))
        {
            tr_variantListAddStr(labels, token);
        }
    }

    tr_free(tmp);
}

static void addFiles(tr_variant* args, tr_quark const key, char const* arg)
{
    tr_variant* files = tr_variantDictAddList(args, key, 100);

    if (tr_str_is_empty(arg))
    {
        fprintf(stderr, "No files specified!\n");
        arg = "-1"; /* no file will have this index, so should be a no-op */
    }

    if (strcmp(arg, "all") != 0)
    {
        int valueCount;
        int* values = tr_parseNumberRange(arg, TR_BAD_SIZE, &valueCount);

        for (int i = 0; i < valueCount; ++i)
        {
            tr_variantListAddInt(files, values[i]);
        }

        tr_free(values);
    }
}

static tr_quark const files_keys[] =
{
    TR_KEY_files,
    TR_KEY_name,
    TR_KEY_priorities,
    TR_KEY_wanted
};

static tr_quark const details_keys[] =
{
    TR_KEY_activityDate,
    TR_KEY_addedDate,
    TR_KEY_bandwidthPriority,
    TR_KEY_comment,
    TR_KEY_corruptEver,
    TR_KEY_creator,
    TR_KEY_dateCreated,
    TR_KEY_desiredAvailable,
    TR_KEY_doneDate,
    TR_KEY_downloadDir,
    TR_KEY_downloadedEver,
    TR_KEY_downloadLimit,
    TR_KEY_downloadLimited,
    TR_KEY_error,
    TR_KEY_errorString,
    TR_KEY_eta,
    TR_KEY_hashString,
    TR_KEY_haveUnchecked,
    TR_KEY_haveValid,
    TR_KEY_honorsSessionLimits,
    TR_KEY_id,
    TR_KEY_isFinished,
    TR_KEY_isPrivate,
    TR_KEY_labels,
    TR_KEY_leftUntilDone,
    TR_KEY_magnetLink,
    TR_KEY_name,
    TR_KEY_peersConnected,
    TR_KEY_peersGettingFromUs,
    TR_KEY_peersSendingToUs,
    TR_KEY_peer_limit,
    TR_KEY_pieceCount,
    TR_KEY_pieceSize,
    TR_KEY_rateDownload,
    TR_KEY_rateUpload,
    TR_KEY_recheckProgress,
    TR_KEY_secondsDownloading,
    TR_KEY_secondsSeeding,
    TR_KEY_seedRatioMode,
    TR_KEY_seedRatioLimit,
    TR_KEY_sizeWhenDone,
    TR_KEY_startDate,
    TR_KEY_status,
    TR_KEY_totalSize,
    TR_KEY_uploadedEver,
    TR_KEY_uploadLimit,
    TR_KEY_uploadLimited,
    TR_KEY_webseeds,
    TR_KEY_webseedsSendingToUs
};

static tr_quark const list_keys[] =
{
    TR_KEY_error,
    TR_KEY_errorString,
    TR_KEY_eta,
    TR_KEY_id,
    TR_KEY_isFinished,
    TR_KEY_leftUntilDone,
    TR_KEY_name,
    TR_KEY_peersGettingFromUs,
    TR_KEY_peersSendingToUs,
    TR_KEY_rateDownload,
    TR_KEY_rateUpload,
    TR_KEY_sizeWhenDone,
    TR_KEY_status,
    TR_KEY_uploadRatio
};

static size_t writeFunc(void* ptr, size_t size, size_t nmemb, void* buf)
{
    size_t const byteCount = size * nmemb;
    evbuffer_add(buf, ptr, byteCount);
    return byteCount;
}

/* look for a session id in the header in case the server gives back a 409 */
static size_t parseResponseHeader(void* ptr, size_t size, size_t nmemb, void* stream UNUSED)
{
    char const* line = ptr;
    size_t const line_len = size * nmemb;
    char const* key = TR_RPC_SESSION_ID_HEADER ": ";
    size_t const key_len = strlen(key);

    if (line_len >= key_len && evutil_ascii_strncasecmp(line, key, key_len) == 0)
    {
        char const* begin = line + key_len;
        char const* end = begin;

        while (!isspace(*end))
        {
            ++end;
        }

        tr_free(sessionId);
        sessionId = tr_strndup(begin, end - begin);
    }

    return line_len;
}

static long getTimeoutSecs(char const* req)
{
    if (strstr(req, "\"method\":\"blocklist-update\"") != NULL)
    {
        return 300L;
    }

    return 60L; /* default value */
}

static char* getStatusString(tr_variant* t, char* buf, size_t buflen)
{
    int64_t status;
    bool boolVal;

    if (!tr_variantDictFindInt(t, TR_KEY_status, &status))
    {
        *buf = '\0';
    }
    else
    {
        switch (status)
        {
        case TR_STATUS_DOWNLOAD_WAIT:
        case TR_STATUS_SEED_WAIT:
            tr_strlcpy(buf, "Queued", buflen);
            break;

        case TR_STATUS_STOPPED:
            if (tr_variantDictFindBool(t, TR_KEY_isFinished, &boolVal) && boolVal)
            {
                tr_strlcpy(buf, "Finished", buflen);
            }
            else
            {
                tr_strlcpy(buf, "Stopped", buflen);
            }

            break;

        case TR_STATUS_CHECK_WAIT:
        case TR_STATUS_CHECK:
            {
                char const* str = status == TR_STATUS_CHECK_WAIT ? "Will Verify" : "Verifying";
                double percent;

                if (tr_variantDictFindReal(t, TR_KEY_recheckProgress, &percent))
                {
                    tr_snprintf(buf, buflen, "%s (%.0f%%)", str, (floor)(percent * 100.0));
                }
                else
                {
                    tr_strlcpy(buf, str, buflen);
                }

                break;
            }

        case TR_STATUS_DOWNLOAD:
        case TR_STATUS_SEED:
            {
                int64_t fromUs = 0;
                int64_t toUs = 0;
                tr_variantDictFindInt(t, TR_KEY_peersGettingFromUs, &fromUs);
                tr_variantDictFindInt(t, TR_KEY_peersSendingToUs, &toUs);

                if (fromUs != 0 && toUs != 0)
                {
                    tr_strlcpy(buf, "Up & Down", buflen);
                }
                else if (toUs != 0)
                {
                    tr_strlcpy(buf, "Downloading", buflen);
                }
                else if (fromUs != 0)
                {
                    int64_t leftUntilDone = 0;
                    tr_variantDictFindInt(t, TR_KEY_leftUntilDone, &leftUntilDone);

                    if (leftUntilDone > 0)
                    {
                        tr_strlcpy(buf, "Uploading", buflen);
                    }
                    else
                    {
                        tr_strlcpy(buf, "Seeding", buflen);
                    }
                }
                else
                {
                    tr_strlcpy(buf, "Idle", buflen);
                }

                break;
            }

        default:
            tr_strlcpy(buf, "Unknown", buflen);
            break;
        }
    }

    return buf;
}

static char const* bandwidthPriorityNames[] =
{
    "Low",
    "Normal",
    "High",
    "Invalid"
};

static void printDetails(tr_variant* top)
{
    tr_variant* args;
    tr_variant* torrents;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &torrents))
    {
        for (int ti = 0, tCount = tr_variantListSize(torrents); ti < tCount; ++ti)
        {
            tr_variant* t = tr_variantListChild(torrents, ti);
            tr_variant* l;
            char const* str;
            char buf[512];
            char buf2[512];
            int64_t i;
            int64_t j;
            int64_t k;
            bool boolVal;
            double d;

            printf("NAME\n");

            if (tr_variantDictFindInt(t, TR_KEY_id, &i))
            {
                printf("  Id: %" PRId64 "\n", i);
            }

            if (tr_variantDictFindStr(t, TR_KEY_name, &str, NULL))
            {
                printf("  Name: %s\n", str);
            }

            if (tr_variantDictFindStr(t, TR_KEY_hashString, &str, NULL))
            {
                printf("  Hash: %s\n", str);
            }

            if (tr_variantDictFindStr(t, TR_KEY_magnetLink, &str, NULL))
            {
                printf("  Magnet: %s\n", str);
            }

            if (tr_variantDictFindList(t, TR_KEY_labels, &l))
            {
                int const n = tr_variantListSize(l);
                char const* str;
                printf("  Labels: ");
                for (int i = 0; i < n; i++)
                {
                    if (tr_variantGetStr(tr_variantListChild(l, i), &str, NULL))
                    {
                        printf(i == 0 ? "%s" : ", %s", str);
                    }
                }

                printf("\n");
            }

            printf("\n");

            printf("TRANSFER\n");
            getStatusString(t, buf, sizeof(buf));
            printf("  State: %s\n", buf);

            if (tr_variantDictFindStr(t, TR_KEY_downloadDir, &str, NULL))
            {
                printf("  Location: %s\n", str);
            }

            if (tr_variantDictFindInt(t, TR_KEY_sizeWhenDone, &i) && tr_variantDictFindInt(t, TR_KEY_leftUntilDone, &j))
            {
                strlpercent(buf, 100.0 * (i - j) / i, sizeof(buf));
                printf("  Percent Done: %s%%\n", buf);
            }

            if (tr_variantDictFindInt(t, TR_KEY_eta, &i))
            {
                printf("  ETA: %s\n", tr_strltime(buf, i, sizeof(buf)));
            }

            if (tr_variantDictFindInt(t, TR_KEY_rateDownload, &i))
            {
                printf("  Download Speed: %s\n", tr_formatter_speed_KBps(buf, i / (double)tr_speed_K, sizeof(buf)));
            }

            if (tr_variantDictFindInt(t, TR_KEY_rateUpload, &i))
            {
                printf("  Upload Speed: %s\n", tr_formatter_speed_KBps(buf, i / (double)tr_speed_K, sizeof(buf)));
            }

            if (tr_variantDictFindInt(t, TR_KEY_haveUnchecked, &i) && tr_variantDictFindInt(t, TR_KEY_haveValid, &j))
            {
                strlsize(buf, i + j, sizeof(buf));
                strlsize(buf2, j, sizeof(buf2));
                printf("  Have: %s (%s verified)\n", buf, buf2);
            }

            if (tr_variantDictFindInt(t, TR_KEY_sizeWhenDone, &i))
            {
                if (i < 1)
                {
                    printf("  Availability: None\n");
                }

                if (tr_variantDictFindInt(t, TR_KEY_desiredAvailable, &j) && tr_variantDictFindInt(t, TR_KEY_leftUntilDone, &k))
                {
                    j += i - k;
                    strlpercent(buf, 100.0 * j / i, sizeof(buf));
                    printf("  Availability: %s%%\n", buf);
                }

                if (tr_variantDictFindInt(t, TR_KEY_totalSize, &j))
                {
                    strlsize(buf2, i, sizeof(buf2));
                    strlsize(buf, j, sizeof(buf));
                    printf("  Total size: %s (%s wanted)\n", buf, buf2);
                }
            }

            if (tr_variantDictFindInt(t, TR_KEY_downloadedEver, &i) && tr_variantDictFindInt(t, TR_KEY_uploadedEver, &j))
            {
                strlsize(buf, i, sizeof(buf));
                printf("  Downloaded: %s\n", buf);
                strlsize(buf, j, sizeof(buf));
                printf("  Uploaded: %s\n", buf);
                strlratio(buf, j, i, sizeof(buf));
                printf("  Ratio: %s\n", buf);
            }

            if (tr_variantDictFindInt(t, TR_KEY_corruptEver, &i))
            {
                strlsize(buf, i, sizeof(buf));
                printf("  Corrupt DL: %s\n", buf);
            }

            if (tr_variantDictFindStr(t, TR_KEY_errorString, &str, NULL) && !tr_str_is_empty(str) &&
                tr_variantDictFindInt(t, TR_KEY_error, &i) && i != 0)
            {
                switch (i)
                {
                case TR_STAT_TRACKER_WARNING:
                    printf("  Tracker gave a warning: %s\n", str);
                    break;

                case TR_STAT_TRACKER_ERROR:
                    printf("  Tracker gave an error: %s\n", str);
                    break;

                case TR_STAT_LOCAL_ERROR:
                    printf("  Error: %s\n", str);
                    break;

                default:
                    break; /* no error */
                }
            }

            if (tr_variantDictFindInt(t, TR_KEY_peersConnected, &i) &&
                tr_variantDictFindInt(t, TR_KEY_peersGettingFromUs, &j) &&
                tr_variantDictFindInt(t, TR_KEY_peersSendingToUs, &k))
            {
                printf("  Peers: connected to %" PRId64 ", uploading to %" PRId64 ", downloading from %" PRId64 "\n", i, j, k);
            }

            if (tr_variantDictFindList(t, TR_KEY_webseeds, &l) && tr_variantDictFindInt(t, TR_KEY_webseedsSendingToUs, &i))
            {
                int64_t const n = tr_variantListSize(l);

                if (n > 0)
                {
                    printf("  Web Seeds: downloading from %" PRId64 " of %" PRId64 " web seeds\n", i, n);
                }
            }

            printf("\n");

            printf("HISTORY\n");

            if (tr_variantDictFindInt(t, TR_KEY_addedDate, &i) && i != 0)
            {
                time_t const tt = i;
                printf("  Date added:       %s", ctime(&tt));
            }

            if (tr_variantDictFindInt(t, TR_KEY_doneDate, &i) && i != 0)
            {
                time_t const tt = i;
                printf("  Date finished:    %s", ctime(&tt));
            }

            if (tr_variantDictFindInt(t, TR_KEY_startDate, &i) && i != 0)
            {
                time_t const tt = i;
                printf("  Date started:     %s", ctime(&tt));
            }

            if (tr_variantDictFindInt(t, TR_KEY_activityDate, &i) && i != 0)
            {
                time_t const tt = i;
                printf("  Latest activity:  %s", ctime(&tt));
            }

            if (tr_variantDictFindInt(t, TR_KEY_secondsDownloading, &i) && i > 0)
            {
                printf("  Downloading Time: %s\n", tr_strltime(buf, i, sizeof(buf)));
            }

            if (tr_variantDictFindInt(t, TR_KEY_secondsSeeding, &i) && i > 0)
            {
                printf("  Seeding Time:     %s\n", tr_strltime(buf, i, sizeof(buf)));
            }

            printf("\n");

            printf("ORIGINS\n");

            if (tr_variantDictFindInt(t, TR_KEY_dateCreated, &i) && i != 0)
            {
                time_t const tt = i;
                printf("  Date created: %s", ctime(&tt));
            }

            if (tr_variantDictFindBool(t, TR_KEY_isPrivate, &boolVal))
            {
                printf("  Public torrent: %s\n", (boolVal ? "No" : "Yes"));
            }

            if (tr_variantDictFindStr(t, TR_KEY_comment, &str, NULL) && !tr_str_is_empty(str))
            {
                printf("  Comment: %s\n", str);
            }

            if (tr_variantDictFindStr(t, TR_KEY_creator, &str, NULL) && !tr_str_is_empty(str))
            {
                printf("  Creator: %s\n", str);
            }

            if (tr_variantDictFindInt(t, TR_KEY_pieceCount, &i))
            {
                printf("  Piece Count: %" PRId64 "\n", i);
            }

            if (tr_variantDictFindInt(t, TR_KEY_pieceSize, &i))
            {
                printf("  Piece Size: %s\n", strlmem(buf, i, sizeof(buf)));
            }

            printf("\n");

            printf("LIMITS & BANDWIDTH\n");

            if (tr_variantDictFindBool(t, TR_KEY_downloadLimited, &boolVal) &&
                tr_variantDictFindInt(t, TR_KEY_downloadLimit, &i))
            {
                printf("  Download Limit: ");

                if (boolVal)
                {
                    printf("%s\n", tr_formatter_speed_KBps(buf, i, sizeof(buf)));
                }
                else
                {
                    printf("Unlimited\n");
                }
            }

            if (tr_variantDictFindBool(t, TR_KEY_uploadLimited, &boolVal) && tr_variantDictFindInt(t, TR_KEY_uploadLimit, &i))
            {
                printf("  Upload Limit: ");

                if (boolVal)
                {
                    printf("%s\n", tr_formatter_speed_KBps(buf, i, sizeof(buf)));
                }
                else
                {
                    printf("Unlimited\n");
                }
            }

            if (tr_variantDictFindInt(t, TR_KEY_seedRatioMode, &i))
            {
                switch (i)
                {
                case TR_RATIOLIMIT_GLOBAL:
                    printf("  Ratio Limit: Default\n");
                    break;

                case TR_RATIOLIMIT_SINGLE:
                    if (tr_variantDictFindReal(t, TR_KEY_seedRatioLimit, &d))
                    {
                        printf("  Ratio Limit: %s\n", strlratio2(buf, d, sizeof(buf)));
                    }

                    break;

                case TR_RATIOLIMIT_UNLIMITED:
                    printf("  Ratio Limit: Unlimited\n");
                    break;

                default:
                    break;
                }
            }

            if (tr_variantDictFindBool(t, TR_KEY_honorsSessionLimits, &boolVal))
            {
                printf("  Honors Session Limits: %s\n", (boolVal ? "Yes" : "No"));
            }

            if (tr_variantDictFindInt(t, TR_KEY_peer_limit, &i))
            {
                printf("  Peer limit: %" PRId64 "\n", i);
            }

            if (tr_variantDictFindInt(t, TR_KEY_bandwidthPriority, &i))
            {
                printf("  Bandwidth Priority: %s\n", bandwidthPriorityNames[(i + 1) & 3]);
            }

            printf("\n");
        }
    }
}

static void printFileList(tr_variant* top)
{
    tr_variant* args;
    tr_variant* torrents;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &torrents))
    {
        for (int i = 0, in = tr_variantListSize(torrents); i < in; ++i)
        {
            tr_variant* d = tr_variantListChild(torrents, i);
            tr_variant* files;
            tr_variant* priorities;
            tr_variant* wanteds;
            char const* name;

            if (tr_variantDictFindStr(d, TR_KEY_name, &name, NULL) && tr_variantDictFindList(d, TR_KEY_files, &files) &&
                tr_variantDictFindList(d, TR_KEY_priorities, &priorities) && tr_variantDictFindList(d, TR_KEY_wanted, &wanteds))
            {
                int const jn = tr_variantListSize(files);
                printf("%s (%d files):\n", name, jn);
                printf("%3s  %4s %8s %3s %9s  %s\n", "#", "Done", "Priority", "Get", "Size", "Name");

                for (int j = 0; j < jn; ++j)
                {
                    int64_t have;
                    int64_t length;
                    int64_t priority;
                    bool wanted;
                    char const* filename;
                    tr_variant* file = tr_variantListChild(files, j);

                    if (tr_variantDictFindInt(file, TR_KEY_length, &length) &&
                        tr_variantDictFindStr(file, TR_KEY_name, &filename, NULL) &&
                        tr_variantDictFindInt(file, TR_KEY_bytesCompleted, &have) &&
                        tr_variantGetInt(tr_variantListChild(priorities, j), &priority) &&
                        tr_variantGetBool(tr_variantListChild(wanteds, j), &wanted))
                    {
                        char sizestr[64];
                        double percent = (double)have / length;
                        char const* pristr;
                        strlsize(sizestr, length, sizeof(sizestr));

                        switch (priority)
                        {
                        case TR_PRI_LOW:
                            pristr = "Low";
                            break;

                        case TR_PRI_HIGH:
                            pristr = "High";
                            break;

                        default:
                            pristr = "Normal";
                            break;
                        }

                        printf("%3d: %3.0f%% %-8s %-3s %9s  %s\n", j, (floor)(100.0 * percent), pristr, wanted ? "Yes" : "No",
                            sizestr, filename);
                    }
                }
            }
        }
    }
}

static void printPeersImpl(tr_variant* peers)
{
    printf("%-40s  %-12s  %-5s %-6s  %-6s  %s\n", "Address", "Flags", "Done", "Down", "Up", "Client");

    for (int i = 0, n = tr_variantListSize(peers); i < n; ++i)
    {
        double progress;
        char const* address;
        char const* client;
        char const* flagstr;
        int64_t rateToClient;
        int64_t rateToPeer;
        tr_variant* d = tr_variantListChild(peers, i);

        if (tr_variantDictFindStr(d, TR_KEY_address, &address, NULL) &&
            tr_variantDictFindStr(d, TR_KEY_clientName, &client, NULL) &&
            tr_variantDictFindReal(d, TR_KEY_progress, &progress) &&
            tr_variantDictFindStr(d, TR_KEY_flagStr, &flagstr, NULL) &&
            tr_variantDictFindInt(d, TR_KEY_rateToClient, &rateToClient) &&
            tr_variantDictFindInt(d, TR_KEY_rateToPeer, &rateToPeer))
        {
            printf("%-40s  %-12s  %-5.1f %6.1f  %6.1f  %s\n", address, flagstr, (progress * 100.0),
                rateToClient / (double)tr_speed_K, rateToPeer / (double)tr_speed_K, client);
        }
    }
}

static void printPeers(tr_variant* top)
{
    tr_variant* args;
    tr_variant* torrents;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &torrents))
    {
        for (int i = 0, n = tr_variantListSize(torrents); i < n; ++i)
        {
            tr_variant* peers;
            tr_variant* torrent = tr_variantListChild(torrents, i);

            if (tr_variantDictFindList(torrent, TR_KEY_peers, &peers))
            {
                printPeersImpl(peers);

                if (i + 1 < n)
                {
                    printf("\n");
                }
            }
        }
    }
}

static void printPiecesImpl(uint8_t const* raw, size_t rawlen, size_t j)
{
    size_t len;
    char* str = tr_base64_decode(raw, rawlen, &len);
    printf("  ");

    for (size_t i = 0, k = 0; k < len; ++k)
    {
        for (int e = 0; i < j && e < 8; ++e, ++i)
        {
            printf("%c", (str[k] & (1 << (7 - e))) != 0 ? '1' : '0');
        }

        printf(" ");

        if (i % 64 == 0)
        {
            printf("\n  ");
        }
    }

    printf("\n");
    tr_free(str);
}

static void printPieces(tr_variant* top)
{
    tr_variant* args;
    tr_variant* torrents;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &torrents))
    {
        for (int i = 0, n = tr_variantListSize(torrents); i < n; ++i)
        {
            int64_t j;
            uint8_t const* raw;
            size_t rawlen;
            tr_variant* torrent = tr_variantListChild(torrents, i);

            if (tr_variantDictFindRaw(torrent, TR_KEY_pieces, &raw, &rawlen) &&
                tr_variantDictFindInt(torrent, TR_KEY_pieceCount, &j))
            {
                assert(j >= 0);
                printPiecesImpl(raw, rawlen, (size_t)j);

                if (i + 1 < n)
                {
                    printf("\n");
                }
            }
        }
    }
}

static void printPortTest(tr_variant* top)
{
    tr_variant* args;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args))
    {
        bool boolVal;

        if (tr_variantDictFindBool(args, TR_KEY_port_is_open, &boolVal))
        {
            printf("Port is open: %s\n", boolVal ? "Yes" : "No");
        }
    }
}

static void printTorrentList(tr_variant* top)
{
    tr_variant* args;
    tr_variant* list;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &list))
    {
        int64_t total_size = 0;
        double total_up = 0;
        double total_down = 0;
        char haveStr[32];

        printf("%6s   %-4s  %9s  %-8s  %6s  %6s  %-5s  %-11s  %s\n", "ID", "Done", "Have", "ETA", "Up", "Down", "Ratio",
            "Status", "Name");

        for (int i = 0, n = tr_variantListSize(list); i < n; ++i)
        {
            int64_t id;
            int64_t eta;
            int64_t status;
            int64_t up;
            int64_t down;
            int64_t sizeWhenDone;
            int64_t leftUntilDone;
            double ratio;
            char const* name;
            tr_variant* d = tr_variantListChild(list, i);

            if (tr_variantDictFindInt(d, TR_KEY_eta, &eta) &&
                tr_variantDictFindInt(d, TR_KEY_id, &id) &&
                tr_variantDictFindInt(d, TR_KEY_leftUntilDone, &leftUntilDone) &&
                tr_variantDictFindStr(d, TR_KEY_name, &name, NULL) &&
                tr_variantDictFindInt(d, TR_KEY_rateDownload, &down) &&
                tr_variantDictFindInt(d, TR_KEY_rateUpload, &up) &&
                tr_variantDictFindInt(d, TR_KEY_sizeWhenDone, &sizeWhenDone) &&
                tr_variantDictFindInt(d, TR_KEY_status, &status) &&
                tr_variantDictFindReal(d, TR_KEY_uploadRatio, &ratio))
            {
                char etaStr[16];
                char statusStr[64];
                char ratioStr[32];
                char doneStr[8];
                int64_t error;
                char errorMark;

                if (sizeWhenDone != 0)
                {
                    tr_snprintf(doneStr, sizeof(doneStr), "%d%%", (int)(100.0 * (sizeWhenDone - leftUntilDone) / sizeWhenDone));
                }
                else
                {
                    tr_strlcpy(doneStr, "n/a", sizeof(doneStr));
                }

                strlsize(haveStr, sizeWhenDone - leftUntilDone, sizeof(haveStr));

                if (leftUntilDone != 0 || eta != -1)
                {
                    etaToString(etaStr, sizeof(etaStr), eta);
                }
                else
                {
                    tr_snprintf(etaStr, sizeof(etaStr), "Done");
                }

                if (tr_variantDictFindInt(d, TR_KEY_error, &error) && error)
                {
                    errorMark = '*';
                }
                else
                {
                    errorMark = ' ';
                }

                printf("%6d%c  %4s  %9s  %-8s  %6.1f  %6.1f  %5s  %-11s  %s\n", (int)id, errorMark, doneStr, haveStr, etaStr,
                    up / (double)tr_speed_K, down / (double)tr_speed_K, strlratio2(ratioStr, ratio, sizeof(ratioStr)),
                    getStatusString(d, statusStr, sizeof(statusStr)), name);

                total_up += up;
                total_down += down;
                total_size += sizeWhenDone - leftUntilDone;
            }
        }

        printf("Sum:           %9s            %6.1f  %6.1f\n", strlsize(haveStr, total_size, sizeof(haveStr)),
            total_up / (double)tr_speed_K, total_down / (double)tr_speed_K);
    }
}

static void printTrackersImpl(tr_variant* trackerStats)
{
    char buf[512];
    tr_variant* t;

    for (int i = 0; (t = tr_variantListChild(trackerStats, i)) != NULL; ++i)
    {
        int64_t downloadCount;
        bool hasAnnounced;
        bool hasScraped;
        char const* host;
        int64_t id;
        bool isBackup;
        int64_t lastAnnouncePeerCount;
        char const* lastAnnounceResult;
        int64_t lastAnnounceStartTime;
        bool lastAnnounceSucceeded;
        int64_t lastAnnounceTime;
        bool lastAnnounceTimedOut;
        char const* lastScrapeResult;
        bool lastScrapeSucceeded;
        int64_t lastScrapeStartTime;
        int64_t lastScrapeTime;
        bool lastScrapeTimedOut;
        int64_t leecherCount;
        int64_t nextAnnounceTime;
        int64_t nextScrapeTime;
        int64_t seederCount;
        int64_t tier;
        int64_t announceState;
        int64_t scrapeState;

        if (tr_variantDictFindInt(t, TR_KEY_downloadCount, &downloadCount) &&
            tr_variantDictFindBool(t, TR_KEY_hasAnnounced, &hasAnnounced) &&
            tr_variantDictFindBool(t, TR_KEY_hasScraped, &hasScraped) &&
            tr_variantDictFindStr(t, TR_KEY_host, &host, NULL) &&
            tr_variantDictFindInt(t, TR_KEY_id, &id) &&
            tr_variantDictFindBool(t, TR_KEY_isBackup, &isBackup) &&
            tr_variantDictFindInt(t, TR_KEY_announceState, &announceState) &&
            tr_variantDictFindInt(t, TR_KEY_scrapeState, &scrapeState) &&
            tr_variantDictFindInt(t, TR_KEY_lastAnnouncePeerCount, &lastAnnouncePeerCount) &&
            tr_variantDictFindStr(t, TR_KEY_lastAnnounceResult, &lastAnnounceResult, NULL) &&
            tr_variantDictFindInt(t, TR_KEY_lastAnnounceStartTime, &lastAnnounceStartTime) &&
            tr_variantDictFindBool(t, TR_KEY_lastAnnounceSucceeded, &lastAnnounceSucceeded) &&
            tr_variantDictFindInt(t, TR_KEY_lastAnnounceTime, &lastAnnounceTime) &&
            tr_variantDictFindBool(t, TR_KEY_lastAnnounceTimedOut, &lastAnnounceTimedOut) &&
            tr_variantDictFindStr(t, TR_KEY_lastScrapeResult, &lastScrapeResult, NULL) &&
            tr_variantDictFindInt(t, TR_KEY_lastScrapeStartTime, &lastScrapeStartTime) &&
            tr_variantDictFindBool(t, TR_KEY_lastScrapeSucceeded, &lastScrapeSucceeded) &&
            tr_variantDictFindInt(t, TR_KEY_lastScrapeTime, &lastScrapeTime) &&
            tr_variantDictFindBool(t, TR_KEY_lastScrapeTimedOut, &lastScrapeTimedOut) &&
            tr_variantDictFindInt(t, TR_KEY_leecherCount, &leecherCount) &&
            tr_variantDictFindInt(t, TR_KEY_nextAnnounceTime, &nextAnnounceTime) &&
            tr_variantDictFindInt(t, TR_KEY_nextScrapeTime, &nextScrapeTime) &&
            tr_variantDictFindInt(t, TR_KEY_seederCount, &seederCount) &&
            tr_variantDictFindInt(t, TR_KEY_tier, &tier))
        {
            time_t const now = time(NULL);

            printf("\n");
            printf("  Tracker %d: %s\n", (int)(id), host);

            if (isBackup)
            {
                printf("  Backup on tier %d\n", (int)tier);
            }
            else
            {
                printf("  Active in tier %d\n", (int)tier);
            }

            if (!isBackup)
            {
                if (hasAnnounced && announceState != TR_TRACKER_INACTIVE)
                {
                    tr_strltime(buf, now - lastAnnounceTime, sizeof(buf));

                    if (lastAnnounceSucceeded)
                    {
                        printf("  Got a list of %d peers %s ago\n", (int)lastAnnouncePeerCount, buf);
                    }
                    else if (lastAnnounceTimedOut)
                    {
                        printf("  Peer list request timed out; will retry\n");
                    }
                    else
                    {
                        printf("  Got an error \"%s\" %s ago\n", lastAnnounceResult, buf);
                    }
                }

                switch (announceState)
                {
                case TR_TRACKER_INACTIVE:
                    printf("  No updates scheduled\n");
                    break;

                case TR_TRACKER_WAITING:
                    tr_strltime(buf, nextAnnounceTime - now, sizeof(buf));
                    printf("  Asking for more peers in %s\n", buf);
                    break;

                case TR_TRACKER_QUEUED:
                    printf("  Queued to ask for more peers\n");
                    break;

                case TR_TRACKER_ACTIVE:
                    tr_strltime(buf, now - lastAnnounceStartTime, sizeof(buf));
                    printf("  Asking for more peers now... %s\n", buf);
                    break;
                }

                if (hasScraped)
                {
                    tr_strltime(buf, now - lastScrapeTime, sizeof(buf));

                    if (lastScrapeSucceeded)
                    {
                        printf("  Tracker had %d seeders and %d leechers %s ago\n", (int)seederCount, (int)leecherCount, buf);
                    }
                    else if (lastScrapeTimedOut)
                    {
                        printf("  Tracker scrape timed out; will retry\n");
                    }
                    else
                    {
                        printf("  Got a scrape error \"%s\" %s ago\n", lastScrapeResult, buf);
                    }
                }

                switch (scrapeState)
                {
                case TR_TRACKER_INACTIVE:
                    break;

                case TR_TRACKER_WAITING:
                    tr_strltime(buf, nextScrapeTime - now, sizeof(buf));
                    printf("  Asking for peer counts in %s\n", buf);
                    break;

                case TR_TRACKER_QUEUED:
                    printf("  Queued to ask for peer counts\n");
                    break;

                case TR_TRACKER_ACTIVE:
                    tr_strltime(buf, now - lastScrapeStartTime, sizeof(buf));
                    printf("  Asking for peer counts now... %s\n", buf);
                    break;
                }
            }
        }
    }
}

static void printTrackers(tr_variant* top)
{
    tr_variant* args;
    tr_variant* torrents;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args) && tr_variantDictFindList(args, TR_KEY_torrents, &torrents))
    {
        for (int i = 0, n = tr_variantListSize(torrents); i < n; ++i)
        {
            tr_variant* trackerStats;
            tr_variant* torrent = tr_variantListChild(torrents, i);

            if (tr_variantDictFindList(torrent, TR_KEY_trackerStats, &trackerStats))
            {
                printTrackersImpl(trackerStats);

                if (i + 1 < n)
                {
                    printf("\n");
                }
            }
        }
    }
}

static void printSession(tr_variant* top)
{
    tr_variant* args;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args))
    {
        int64_t i;
        char buf[64];
        bool boolVal;
        char const* str;

        printf("VERSION\n");

        if (tr_variantDictFindStr(args, TR_KEY_version, &str, NULL))
        {
            printf("  Daemon version: %s\n", str);
        }

        if (tr_variantDictFindInt(args, TR_KEY_rpc_version, &i))
        {
            printf("  RPC version: %" PRId64 "\n", i);
        }

        if (tr_variantDictFindInt(args, TR_KEY_rpc_version_minimum, &i))
        {
            printf("  RPC minimum version: %" PRId64 "\n", i);
        }

        printf("\n");

        printf("CONFIG\n");

        if (tr_variantDictFindStr(args, TR_KEY_config_dir, &str, NULL))
        {
            printf("  Configuration directory: %s\n", str);
        }

        if (tr_variantDictFindStr(args, TR_KEY_download_dir, &str, NULL))
        {
            printf("  Download directory: %s\n", str);
        }

        if (tr_variantDictFindInt(args, TR_KEY_peer_port, &i))
        {
            printf("  Listenport: %" PRId64 "\n", i);
        }

        if (tr_variantDictFindBool(args, TR_KEY_port_forwarding_enabled, &boolVal))
        {
            printf("  Portforwarding enabled: %s\n", boolVal ? "Yes" : "No");
        }

        if (tr_variantDictFindBool(args, TR_KEY_utp_enabled, &boolVal))
        {
            printf("  uTP enabled: %s\n", (boolVal ? "Yes" : "No"));
        }

        if (tr_variantDictFindBool(args, TR_KEY_dht_enabled, &boolVal))
        {
            printf("  Distributed hash table enabled: %s\n", boolVal ? "Yes" : "No");
        }

        if (tr_variantDictFindBool(args, TR_KEY_lpd_enabled, &boolVal))
        {
            printf("  Local peer discovery enabled: %s\n", boolVal ? "Yes" : "No");
        }

        if (tr_variantDictFindBool(args, TR_KEY_pex_enabled, &boolVal))
        {
            printf("  Peer exchange allowed: %s\n", boolVal ? "Yes" : "No");
        }

        if (tr_variantDictFindStr(args, TR_KEY_encryption, &str, NULL))
        {
            printf("  Encryption: %s\n", str);
        }

        if (tr_variantDictFindInt(args, TR_KEY_cache_size_mb, &i))
        {
            printf("  Maximum memory cache size: %s\n", tr_formatter_mem_MB(buf, i, sizeof(buf)));
        }

        printf("\n");

        {
            bool altEnabled;
            bool altTimeEnabled;
            bool upEnabled;
            bool downEnabled;
            bool seedRatioLimited;
            int64_t altDown;
            int64_t altUp;
            int64_t altBegin;
            int64_t altEnd;
            int64_t altDay;
            int64_t upLimit;
            int64_t downLimit;
            int64_t peerLimit;
            double seedRatioLimit;

            if (tr_variantDictFindInt(args, TR_KEY_alt_speed_down, &altDown) &&
                tr_variantDictFindBool(args, TR_KEY_alt_speed_enabled, &altEnabled) &&
                tr_variantDictFindInt(args, TR_KEY_alt_speed_time_begin, &altBegin) &&
                tr_variantDictFindBool(args, TR_KEY_alt_speed_time_enabled, &altTimeEnabled) &&
                tr_variantDictFindInt(args, TR_KEY_alt_speed_time_end, &altEnd) &&
                tr_variantDictFindInt(args, TR_KEY_alt_speed_time_day, &altDay) &&
                tr_variantDictFindInt(args, TR_KEY_alt_speed_up, &altUp) &&
                tr_variantDictFindInt(args, TR_KEY_peer_limit_global, &peerLimit) &&
                tr_variantDictFindInt(args, TR_KEY_speed_limit_down, &downLimit) &&
                tr_variantDictFindBool(args, TR_KEY_speed_limit_down_enabled, &downEnabled) &&
                tr_variantDictFindInt(args, TR_KEY_speed_limit_up, &upLimit) &&
                tr_variantDictFindBool(args, TR_KEY_speed_limit_up_enabled, &upEnabled) &&
                tr_variantDictFindReal(args, TR_KEY_seedRatioLimit, &seedRatioLimit) &&
                tr_variantDictFindBool(args, TR_KEY_seedRatioLimited, &seedRatioLimited))
            {
                char buf[128];
                char buf2[128];
                char buf3[128];

                printf("LIMITS\n");
                printf("  Peer limit: %" PRId64 "\n", peerLimit);

                if (seedRatioLimited)
                {
                    strlratio2(buf, seedRatioLimit, sizeof(buf));
                }
                else
                {
                    tr_strlcpy(buf, "Unlimited", sizeof(buf));
                }

                printf("  Default seed ratio limit: %s\n", buf);

                if (altEnabled)
                {
                    tr_formatter_speed_KBps(buf, altUp, sizeof(buf));
                }
                else if (upEnabled)
                {
                    tr_formatter_speed_KBps(buf, upLimit, sizeof(buf));
                }
                else
                {
                    tr_strlcpy(buf, "Unlimited", sizeof(buf));
                }

                printf("  Upload speed limit: %s (%s limit: %s; %s turtle limit: %s)\n", buf,
                    upEnabled ? "Enabled" : "Disabled", tr_formatter_speed_KBps(buf2, upLimit, sizeof(buf2)),
                    altEnabled ? "Enabled" : "Disabled", tr_formatter_speed_KBps(buf3, altUp, sizeof(buf3)));

                if (altEnabled)
                {
                    tr_formatter_speed_KBps(buf, altDown, sizeof(buf));
                }
                else if (downEnabled)
                {
                    tr_formatter_speed_KBps(buf, downLimit, sizeof(buf));
                }
                else
                {
                    tr_strlcpy(buf, "Unlimited", sizeof(buf));
                }

                printf("  Download speed limit: %s (%s limit: %s; %s turtle limit: %s)\n", buf,
                    downEnabled ? "Enabled" : "Disabled", tr_formatter_speed_KBps(buf2, downLimit, sizeof(buf2)),
                    altEnabled ? "Enabled" : "Disabled", tr_formatter_speed_KBps(buf3, altDown, sizeof(buf3)));

                if (altTimeEnabled)
                {
                    printf("  Turtle schedule: %02d:%02d - %02d:%02d  ", (int)(altBegin / 60), (int)(altBegin % 60),
                        (int)(altEnd / 60), (int)(altEnd % 60));

                    if ((altDay & TR_SCHED_SUN) != 0)
                    {
                        printf("Sun ");
                    }

                    if ((altDay & TR_SCHED_MON) != 0)
                    {
                        printf("Mon ");
                    }

                    if ((altDay & TR_SCHED_TUES) != 0)
                    {
                        printf("Tue ");
                    }

                    if ((altDay & TR_SCHED_WED) != 0)
                    {
                        printf("Wed ");
                    }

                    if ((altDay & TR_SCHED_THURS) != 0)
                    {
                        printf("Thu ");
                    }

                    if ((altDay & TR_SCHED_FRI) != 0)
                    {
                        printf("Fri ");
                    }

                    if ((altDay & TR_SCHED_SAT) != 0)
                    {
                        printf("Sat ");
                    }

                    printf("\n");
                }
            }
        }

        printf("\n");

        printf("MISC\n");

        if (tr_variantDictFindBool(args, TR_KEY_start_added_torrents, &boolVal))
        {
            printf("  Autostart added torrents: %s\n", boolVal ? "Yes" : "No");
        }

        if (tr_variantDictFindBool(args, TR_KEY_trash_original_torrent_files, &boolVal))
        {
            printf("  Delete automatically added torrents: %s\n", boolVal ? "Yes" : "No");
        }
    }
}

static void printSessionStats(tr_variant* top)
{
    tr_variant* args;
    tr_variant* d;

    if (tr_variantDictFindDict(top, TR_KEY_arguments, &args))
    {
        char buf[512];
        int64_t up;
        int64_t down;
        int64_t secs;
        int64_t sessions;

        if (tr_variantDictFindDict(args, TR_KEY_current_stats, &d) && tr_variantDictFindInt(d, TR_KEY_uploadedBytes, &up) &&
            tr_variantDictFindInt(d, TR_KEY_downloadedBytes, &down) && tr_variantDictFindInt(d, TR_KEY_secondsActive, &secs))
        {
            printf("\nCURRENT SESSION\n");
            printf("  Uploaded:   %s\n", strlsize(buf, up, sizeof(buf)));
            printf("  Downloaded: %s\n", strlsize(buf, down, sizeof(buf)));
            printf("  Ratio:      %s\n", strlratio(buf, up, down, sizeof(buf)));
            printf("  Duration:   %s\n", tr_strltime(buf, secs, sizeof(buf)));
        }

        if (tr_variantDictFindDict(args, TR_KEY_cumulative_stats, &d) &&
            tr_variantDictFindInt(d, TR_KEY_sessionCount, &sessions) &&
            tr_variantDictFindInt(d, TR_KEY_uploadedBytes, &up) &&
            tr_variantDictFindInt(d, TR_KEY_downloadedBytes, &down) &&
            tr_variantDictFindInt(d, TR_KEY_secondsActive, &secs))
        {
            printf("\nTOTAL\n");
            printf("  Started %lu times\n", (unsigned long)sessions);
            printf("  Uploaded:   %s\n", strlsize(buf, up, sizeof(buf)));
            printf("  Downloaded: %s\n", strlsize(buf, down, sizeof(buf)));
            printf("  Ratio:      %s\n", strlratio(buf, up, down, sizeof(buf)));
            printf("  Duration:   %s\n", tr_strltime(buf, secs, sizeof(buf)));
        }
    }
}

static char id[4096];

static int processResponse(char const* rpcurl, void const* response, size_t len)
{
    tr_variant top;
    int status = EXIT_SUCCESS;

    if (debug)
    {
        fprintf(stderr, "got response (len %d):\n--------\n%*.*s\n--------\n", (int)len, (int)len, (int)len,
            (char const*)response);
    }

    if (tr_variantFromJson(&top, response, len) != 0)
    {
        tr_logAddNamedError(MY_NAME, "Unable to parse response \"%*.*s\"", (int)len, (int)len, (char const*)response);
        status |= EXIT_FAILURE;
    }
    else
    {
        int64_t tag = -1;
        char const* str;

        if (tr_variantDictFindStr(&top, TR_KEY_result, &str, NULL))
        {
            if (strcmp(str, "success") != 0)
            {
                printf("Error: %s\n", str);
                status |= EXIT_FAILURE;
            }
            else
            {
                tr_variantDictFindInt(&top, TR_KEY_tag, &tag);

                switch (tag)
                {
                case TAG_SESSION:
                    printSession(&top);
                    break;

                case TAG_STATS:
                    printSessionStats(&top);
                    break;

                case TAG_DETAILS:
                    printDetails(&top);
                    break;

                case TAG_FILES:
                    printFileList(&top);
                    break;

                case TAG_LIST:
                    printTorrentList(&top);
                    break;

                case TAG_PEERS:
                    printPeers(&top);
                    break;

                case TAG_PIECES:
                    printPieces(&top);
                    break;

                case TAG_PORTTEST:
                    printPortTest(&top);
                    break;

                case TAG_TRACKERS:
                    printTrackers(&top);
                    break;

                case TAG_TORRENT_ADD:
                    {
                        int64_t i;
                        tr_variant* b = &top;

                        if (tr_variantDictFindDict(&top, ARGUMENTS, &b) &&
                            tr_variantDictFindDict(b, TR_KEY_torrent_added, &b) &&
                            tr_variantDictFindInt(b, TR_KEY_id, &i))
                        {
                            tr_snprintf(id, sizeof(id), "%" PRId64, i);
                        }

                        /* fall-through to default: to give success or failure msg */
                        TR_GNUC_FALLTHROUGH;
                    }

                default:
                    if (!tr_variantDictFindStr(&top, TR_KEY_result, &str, NULL))
                    {
                        status |= EXIT_FAILURE;
                    }
                    else
                    {
                        printf("%s responded: \"%s\"\n", rpcurl, str);

                        if (strcmp(str, "success") != 0)
                        {
                            status |= EXIT_FAILURE;
                        }
                    }
                }

                tr_variantFree(&top);
            }
        }
        else
        {
            status |= EXIT_FAILURE;
        }
    }

    return status;
}

static CURL* tr_curl_easy_init(struct evbuffer* writebuf)
{
    CURL* curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_USERAGENT, MY_NAME "/" LONG_VERSION_STRING);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, writebuf);
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, parseResponseHeader);
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
    curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, debug);
    curl_easy_setopt(curl, CURLOPT_ENCODING, ""); /* "" tells curl to fill in the blanks with what it was compiled to support */

    if (netrc != NULL)
    {
        curl_easy_setopt(curl, CURLOPT_NETRC_FILE, netrc);
    }

    if (auth != NULL)
    {
        curl_easy_setopt(curl, CURLOPT_USERPWD, auth);
    }

    if (UseSSL)
    {
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); /* do not verify subject/hostname */
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); /* since most certs will be self-signed, do not verify against CA */
    }

    if (sessionId != NULL)
    {
        char* h = tr_strdup_printf("%s: %s", TR_RPC_SESSION_ID_HEADER, sessionId);
        struct curl_slist* custom_headers = curl_slist_append(NULL, h);
        tr_free(h);

        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, custom_headers);
        curl_easy_setopt(curl, CURLOPT_PRIVATE, custom_headers);
    }

    return curl;
}

static void tr_curl_easy_cleanup(CURL* curl)
{
    struct curl_slist* custom_headers = NULL;
    curl_easy_getinfo(curl, CURLINFO_PRIVATE, &custom_headers);

    curl_easy_cleanup(curl);

    if (custom_headers != NULL)
    {
        curl_slist_free_all(custom_headers);
    }
}

static int flush(char const* rpcurl, tr_variant** benc)
{
    CURLcode res;
    CURL* curl;
    int status = EXIT_SUCCESS;
    struct evbuffer* buf = evbuffer_new();
    char* json = tr_variantToStr(*benc, TR_VARIANT_FMT_JSON_LEAN, NULL);
    char* rpcurl_http = tr_strdup_printf(UseSSL ? "https://%s" : "http://%s", rpcurl);

    curl = tr_curl_easy_init(buf);
    curl_easy_setopt(curl, CURLOPT_URL, rpcurl_http);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, getTimeoutSecs(json));

    if (debug)
    {
        fprintf(stderr, "posting:\n--------\n%s\n--------\n", json);
    }

    if ((res = curl_easy_perform(curl)) != CURLE_OK)
    {
        tr_logAddNamedError(MY_NAME, " (%s) %s", rpcurl_http, curl_easy_strerror(res));
        status |= EXIT_FAILURE;
    }
    else
    {
        long response;
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);

        switch (response)
        {
        case 200:
            status |= processResponse(rpcurl, (char const*)evbuffer_pullup(buf, -1), evbuffer_get_length(buf));
            break;

        case 409:
            /* Session id failed. Our curl header func has already
             * pulled the new session id from this response's headers,
             * build a new CURL* and try again */
            tr_curl_easy_cleanup(curl);
            curl = NULL;
            status |= flush(rpcurl, benc);
            benc = NULL;
            break;

        default:
            evbuffer_add(buf, "", 1);
            fprintf(stderr, "Unexpected response: %s\n", evbuffer_pullup(buf, -1));
            status |= EXIT_FAILURE;
            break;
        }
    }

    /* cleanup */
    tr_free(rpcurl_http);
    tr_free(json);
    evbuffer_free(buf);

    if (curl != NULL)
    {
        tr_curl_easy_cleanup(curl);
    }

    if (benc != NULL)
    {
        tr_variantFree(*benc);
        tr_free(*benc);
        *benc = NULL;
    }

    return status;
}

static tr_variant* ensure_sset(tr_variant** sset)
{
    tr_variant* args;

    if (*sset != NULL)
    {
        args = tr_variantDictFind(*sset, ARGUMENTS);
    }
    else
    {
        *sset = tr_new0(tr_variant, 1);
        tr_variantInitDict(*sset, 3);
        tr_variantDictAddStr(*sset, TR_KEY_method, "session-set");
        args = tr_variantDictAddDict(*sset, ARGUMENTS, 0);
    }

    return args;
}

static tr_variant* ensure_tset(tr_variant** tset)
{
    tr_variant* args;

    if (*tset != NULL)
    {
        args = tr_variantDictFind(*tset, ARGUMENTS);
    }
    else
    {
        *tset = tr_new0(tr_variant, 1);
        tr_variantInitDict(*tset, 3);
        tr_variantDictAddStr(*tset, TR_KEY_method, "torrent-set");
        args = tr_variantDictAddDict(*tset, ARGUMENTS, 1);
    }

    return args;
}

static int processArgs(char const* rpcurl, int argc, char const* const* argv)
{
    int c;
    int status = EXIT_SUCCESS;
    char const* optarg;
    tr_variant* sset = NULL;
    tr_variant* tset = NULL;
    tr_variant* tadd = NULL;

    *id = '\0';

    while ((c = tr_getopt(getUsage(), argc, argv, opts, &optarg)) != TR_OPT_DONE)
    {
        int const stepMode = getOptMode(c);

        if (stepMode == 0) /* meta commands */
        {
            switch (c)
            {
            case 'a': /* add torrent */
                if (sset != NULL)
                {
                    status |= flush(rpcurl, &sset);
                }

                if (tadd != NULL)
                {
                    status |= flush(rpcurl, &tadd);
                }

                if (tset != NULL)
                {
                    addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
                    status |= flush(rpcurl, &tset);
                }

                tadd = tr_new0(tr_variant, 1);
                tr_variantInitDict(tadd, 3);
                tr_variantDictAddStr(tadd, TR_KEY_method, "torrent-add");
                tr_variantDictAddInt(tadd, TR_KEY_tag, TAG_TORRENT_ADD);
                tr_variantDictAddDict(tadd, ARGUMENTS, 0);
                break;

            case 'b': /* debug */
                debug = true;
                break;

            case 'n': /* auth */
                auth = tr_strdup(optarg);
                break;

            case 810: /* authenv */
                auth = tr_env_get_string("TR_AUTH", NULL);

                if (auth == NULL)
                {
                    fprintf(stderr, "The TR_AUTH environment variable is not set\n");
                    exit(0);
                }

                break;

            case 'N': /* netrc */
                netrc = tr_strdup(optarg);
                break;

            case 820: /* UseSSL */
                UseSSL = true;
                break;

            case 't': /* set current torrent */
                if (tadd != NULL)
                {
                    status |= flush(rpcurl, &tadd);
                }

                if (tset != NULL)
                {
                    addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
                    status |= flush(rpcurl, &tset);
                }

                tr_strlcpy(id, optarg, sizeof(id));
                break;

            case 'V': /* show version number */
                fprintf(stderr, "%s %s\n", MY_NAME, LONG_VERSION_STRING);
                exit(0);
                break;

            case TR_OPT_ERR:
                fprintf(stderr, "invalid option\n");
                showUsage();
                status |= EXIT_FAILURE;
                break;

            case TR_OPT_UNK:
                if (tadd != NULL)
                {
                    tr_variant* args = tr_variantDictFind(tadd, ARGUMENTS);
                    char* tmp = getEncodedMetainfo(optarg);

                    if (tmp != NULL)
                    {
                        tr_variantDictAddStr(args, TR_KEY_metainfo, tmp);
                    }
                    else
                    {
                        tr_variantDictAddStr(args, TR_KEY_filename, optarg);
                    }

                    tr_free(tmp);
                }
                else
                {
                    fprintf(stderr, "Unknown option: %s\n", optarg);
                    status |= EXIT_FAILURE;
                }

                break;
            }
        }
        else if (stepMode == MODE_TORRENT_GET)
        {
            tr_variant* top = tr_new0(tr_variant, 1);
            tr_variant* args;
            tr_variant* fields;
            tr_variantInitDict(top, 3);
            tr_variantDictAddStr(top, TR_KEY_method, "torrent-get");
            args = tr_variantDictAddDict(top, ARGUMENTS, 0);
            fields = tr_variantDictAddList(args, TR_KEY_fields, 0);

            if (tset != NULL)
            {
                addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
                status |= flush(rpcurl, &tset);
            }

            switch (c)
            {
            case 'i':
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_DETAILS);

                for (size_t i = 0; i < TR_N_ELEMENTS(details_keys); ++i)
                {
                    tr_variantListAddQuark(fields, details_keys[i]);
                }

                addIdArg(args, id, NULL);
                break;

            case 'l':
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_LIST);

                for (size_t i = 0; i < TR_N_ELEMENTS(list_keys); ++i)
                {
                    tr_variantListAddQuark(fields, list_keys[i]);
                }

                addIdArg(args, id, "all");
                break;

            case 940:
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_FILES);

                for (size_t i = 0; i < TR_N_ELEMENTS(files_keys); ++i)
                {
                    tr_variantListAddQuark(fields, files_keys[i]);
                }

                addIdArg(args, id, NULL);
                break;

            case 941:
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_PEERS);
                tr_variantListAddStr(fields, "peers");
                addIdArg(args, id, NULL);
                break;

            case 942:
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_PIECES);
                tr_variantListAddStr(fields, "pieces");
                tr_variantListAddStr(fields, "pieceCount");
                addIdArg(args, id, NULL);
                break;

            case 943:
                tr_variantDictAddInt(top, TR_KEY_tag, TAG_TRACKERS);
                tr_variantListAddStr(fields, "trackerStats");
                addIdArg(args, id, NULL);
                break;

            default:
                assert("unhandled value" && 0);
            }

            status |= flush(rpcurl, &top);
        }
        else if (stepMode == MODE_SESSION_SET)
        {
            tr_variant* args = ensure_sset(&sset);

            switch (c)
            {
            case 800:
                tr_variantDictAddStr(args, TR_KEY_script_torrent_done_filename, optarg);
                tr_variantDictAddBool(args, TR_KEY_script_torrent_done_enabled, true);
                break;

            case 801:
                tr_variantDictAddBool(args, TR_KEY_script_torrent_done_enabled, false);
                break;

            case 970:
                tr_variantDictAddBool(args, TR_KEY_alt_speed_enabled, true);
                break;

            case 971:
                tr_variantDictAddBool(args, TR_KEY_alt_speed_enabled, false);
                break;

            case 972:
                tr_variantDictAddInt(args, TR_KEY_alt_speed_down, numarg(optarg));
                break;

            case 973:
                tr_variantDictAddInt(args, TR_KEY_alt_speed_up, numarg(optarg));
                break;

            case 974:
                tr_variantDictAddBool(args, TR_KEY_alt_speed_time_enabled, true);
                break;

            case 975:
                tr_variantDictAddBool(args, TR_KEY_alt_speed_time_enabled, false);
                break;

            case 976:
                addTime(args, TR_KEY_alt_speed_time_begin, optarg);
                break;

            case 977:
                addTime(args, TR_KEY_alt_speed_time_end, optarg);
                break;

            case 978:
                addDays(args, TR_KEY_alt_speed_time_day, optarg);
                break;

            case 'c':
                tr_variantDictAddStr(args, TR_KEY_incomplete_dir, optarg);
                tr_variantDictAddBool(args, TR_KEY_incomplete_dir_enabled, true);
                break;

            case 'C':
                tr_variantDictAddBool(args, TR_KEY_incomplete_dir_enabled, false);
                break;

            case 'e':
                tr_variantDictAddInt(args, TR_KEY_cache_size_mb, atoi(optarg));
                break;

            case 910:
                tr_variantDictAddStr(args, TR_KEY_encryption, "required");
                break;

            case 911:
                tr_variantDictAddStr(args, TR_KEY_encryption, "preferred");
                break;

            case 912:
                tr_variantDictAddStr(args, TR_KEY_encryption, "tolerated");
                break;

            case 'm':
                tr_variantDictAddBool(args, TR_KEY_port_forwarding_enabled, true);
                break;

            case 'M':
                tr_variantDictAddBool(args, TR_KEY_port_forwarding_enabled, false);
                break;

            case 'o':
                tr_variantDictAddBool(args, TR_KEY_dht_enabled, true);
                break;

            case 'O':
                tr_variantDictAddBool(args, TR_KEY_dht_enabled, false);
                break;

            case 830:
                tr_variantDictAddBool(args, TR_KEY_utp_enabled, true);
                break;

            case 831:
                tr_variantDictAddBool(args, TR_KEY_utp_enabled, false);
                break;

            case 'p':
                tr_variantDictAddInt(args, TR_KEY_peer_port, numarg(optarg));
                break;

            case 'P':
                tr_variantDictAddBool(args, TR_KEY_peer_port_random_on_start, true);
                break;

            case 'x':
                tr_variantDictAddBool(args, TR_KEY_pex_enabled, true);
                break;

            case 'X':
                tr_variantDictAddBool(args, TR_KEY_pex_enabled, false);
                break;

            case 'y':
                tr_variantDictAddBool(args, TR_KEY_lpd_enabled, true);
                break;

            case 'Y':
                tr_variantDictAddBool(args, TR_KEY_lpd_enabled, false);
                break;

            case 953:
                tr_variantDictAddReal(args, TR_KEY_seedRatioLimit, atof(optarg));
                tr_variantDictAddBool(args, TR_KEY_seedRatioLimited, true);
                break;

            case 954:
                tr_variantDictAddBool(args, TR_KEY_seedRatioLimited, false);
                break;

            case 990:
                tr_variantDictAddBool(args, TR_KEY_start_added_torrents, false);
                break;

            case 991:
                tr_variantDictAddBool(args, TR_KEY_start_added_torrents, true);
                break;

            case 992:
                tr_variantDictAddBool(args, TR_KEY_trash_original_torrent_files, true);
                break;

            case 993:
                tr_variantDictAddBool(args, TR_KEY_trash_original_torrent_files, false);
                break;

            default:
                assert("unhandled value" && 0);
                break;
            }
        }
        else if (stepMode == (MODE_SESSION_SET | MODE_TORRENT_SET))
        {
            tr_variant* targs = NULL;
            tr_variant* sargs = NULL;

            if (!tr_str_is_empty(id))
            {
                targs = ensure_tset(&tset);
            }
            else
            {
                sargs = ensure_sset(&sset);
            }

            switch (c)
            {
            case 'd':
                if (targs != NULL)
                {
                    tr_variantDictAddInt(targs, TR_KEY_downloadLimit, numarg(optarg));
                    tr_variantDictAddBool(targs, TR_KEY_downloadLimited, true);
                }
                else
                {
                    tr_variantDictAddInt(sargs, TR_KEY_speed_limit_down, numarg(optarg));
                    tr_variantDictAddBool(sargs, TR_KEY_speed_limit_down_enabled, true);
                }

                break;

            case 'D':
                if (targs != NULL)
                {
                    tr_variantDictAddBool(targs, TR_KEY_downloadLimited, false);
                }
                else
                {
                    tr_variantDictAddBool(sargs, TR_KEY_speed_limit_down_enabled, false);
                }

                break;

            case 'u':
                if (targs != NULL)
                {
                    tr_variantDictAddInt(targs, TR_KEY_uploadLimit, numarg(optarg));
                    tr_variantDictAddBool(targs, TR_KEY_uploadLimited, true);
                }
                else
                {
                    tr_variantDictAddInt(sargs, TR_KEY_speed_limit_up, numarg(optarg));
                    tr_variantDictAddBool(sargs, TR_KEY_speed_limit_up_enabled, true);
                }

                break;

            case 'U':
                if (targs != NULL)
                {
                    tr_variantDictAddBool(targs, TR_KEY_uploadLimited, false);
                }
                else
                {
                    tr_variantDictAddBool(sargs, TR_KEY_speed_limit_up_enabled, false);
                }

                break;

            case 930:
                if (targs != NULL)
                {
                    tr_variantDictAddInt(targs, TR_KEY_peer_limit, atoi(optarg));
                }
                else
                {
                    tr_variantDictAddInt(sargs, TR_KEY_peer_limit_global, atoi(optarg));
                }

                break;

            default:
                assert("unhandled value" && 0);
                break;
            }
        }
        else if (stepMode == MODE_TORRENT_SET)
        {
            tr_variant* args = ensure_tset(&tset);

            switch (c)
            {
            case 'L':
                addLabels(args, optarg);
                break;

            case 712:
                tr_variantListAddInt(tr_variantDictAddList(args, TR_KEY_trackerRemove, 1), atoi(optarg));
                break;

            case 950:
                tr_variantDictAddReal(args, TR_KEY_seedRatioLimit, atof(optarg));
                tr_variantDictAddInt(args, TR_KEY_seedRatioMode, TR_RATIOLIMIT_SINGLE);
                break;

            case 951:
                tr_variantDictAddInt(args, TR_KEY_seedRatioMode, TR_RATIOLIMIT_GLOBAL);
                break;

            case 952:
                tr_variantDictAddInt(args, TR_KEY_seedRatioMode, TR_RATIOLIMIT_UNLIMITED);
                break;

            case 984:
                tr_variantDictAddBool(args, TR_KEY_honorsSessionLimits, true);
                break;

            case 985:
                tr_variantDictAddBool(args, TR_KEY_honorsSessionLimits, false);
                break;

            default:
                assert("unhandled value" && 0);
                break;
            }
        }
        else if (stepMode == (MODE_TORRENT_SET | MODE_TORRENT_ADD))
        {
            tr_variant* args;

            if (tadd != NULL)
            {
                args = tr_variantDictFind(tadd, ARGUMENTS);
            }
            else
            {
                args = ensure_tset(&tset);
            }

            switch (c)
            {
            case 'g':
                addFiles(args, TR_KEY_files_wanted, optarg);
                break;

            case 'G':
                addFiles(args, TR_KEY_files_unwanted, optarg);
                break;

            case 900:
                addFiles(args, TR_KEY_priority_high, optarg);
                break;

            case 901:
                addFiles(args, TR_KEY_priority_normal, optarg);
                break;

            case 902:
                addFiles(args, TR_KEY_priority_low, optarg);
                break;

            case 700:
                tr_variantDictAddInt(args, TR_KEY_bandwidthPriority, 1);
                break;

            case 701:
                tr_variantDictAddInt(args, TR_KEY_bandwidthPriority, 0);
                break;

            case 702:
                tr_variantDictAddInt(args, TR_KEY_bandwidthPriority, -1);
                break;

            case 710:
                tr_variantListAddStr(tr_variantDictAddList(args, TR_KEY_trackerAdd, 1), optarg);
                break;

            default:
                assert("unhandled value" && 0);
                break;
            }
        }
        else if (c == 961) /* set location */
        {
            if (tadd != NULL)
            {
                tr_variant* args = tr_variantDictFind(tadd, ARGUMENTS);
                tr_variantDictAddStr(args, TR_KEY_download_dir, optarg);
            }
            else
            {
                tr_variant* args;
                tr_variant* top = tr_new0(tr_variant, 1);
                tr_variantInitDict(top, 2);
                tr_variantDictAddStr(top, TR_KEY_method, "torrent-set-location");
                args = tr_variantDictAddDict(top, ARGUMENTS, 3);
                tr_variantDictAddStr(args, TR_KEY_location, optarg);
                tr_variantDictAddBool(args, TR_KEY_move, false);
                addIdArg(args, id, NULL);
                status |= flush(rpcurl, &top);
                break;
            }
        }
        else
        {
            switch (c)
            {
            case 920: /* session-info */
                {
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "session-get");
                    tr_variantDictAddInt(top, TR_KEY_tag, TAG_SESSION);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 's': /* start */
                {
                    if (tadd != NULL)
                    {
                        tr_variantDictAddBool(tr_variantDictFind(tadd, TR_KEY_arguments), TR_KEY_paused, false);
                    }
                    else
                    {
                        tr_variant* top = tr_new0(tr_variant, 1);
                        tr_variantInitDict(top, 2);
                        tr_variantDictAddStr(top, TR_KEY_method, "torrent-start");
                        addIdArg(tr_variantDictAddDict(top, ARGUMENTS, 1), id, NULL);
                        status |= flush(rpcurl, &top);
                    }

                    break;
                }

            case 'S': /* stop */
                {
                    if (tadd != NULL)
                    {
                        tr_variantDictAddBool(tr_variantDictFind(tadd, TR_KEY_arguments), TR_KEY_paused, true);
                    }
                    else
                    {
                        tr_variant* top = tr_new0(tr_variant, 1);
                        tr_variantInitDict(top, 2);
                        tr_variantDictAddStr(top, TR_KEY_method, "torrent-stop");
                        addIdArg(tr_variantDictAddDict(top, ARGUMENTS, 1), id, NULL);
                        status |= flush(rpcurl, &top);
                    }

                    break;
                }

            case 'w':
                {
                    tr_variant* args = tadd != NULL ? tr_variantDictFind(tadd, TR_KEY_arguments) : ensure_sset(&sset);
                    tr_variantDictAddStr(args, TR_KEY_download_dir, optarg);
                    break;
                }

            case 850:
                {
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 1);
                    tr_variantDictAddStr(top, TR_KEY_method, "session-close");
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 963:
                {
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 1);
                    tr_variantDictAddStr(top, TR_KEY_method, "blocklist-update");
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 921:
                {
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "session-stats");
                    tr_variantDictAddInt(top, TR_KEY_tag, TAG_STATS);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 962:
                {
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "port-test");
                    tr_variantDictAddInt(top, TR_KEY_tag, TAG_PORTTEST);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 600:
                {
                    tr_variant* top;

                    if (tset != NULL)
                    {
                        addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
                        status |= flush(rpcurl, &tset);
                    }

                    top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "torrent-reannounce");
                    addIdArg(tr_variantDictAddDict(top, ARGUMENTS, 1), id, NULL);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 'v':
                {
                    tr_variant* top;

                    if (tset != NULL)
                    {
                        addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
                        status |= flush(rpcurl, &tset);
                    }

                    top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "torrent-verify");
                    addIdArg(tr_variantDictAddDict(top, ARGUMENTS, 1), id, NULL);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 'r':
            case 840:
                {
                    tr_variant* args;
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "torrent-remove");
                    args = tr_variantDictAddDict(top, ARGUMENTS, 2);
                    tr_variantDictAddBool(args, TR_KEY_delete_local_data, c == 840);
                    addIdArg(args, id, NULL);
                    status |= flush(rpcurl, &top);
                    break;
                }

            case 960:
                {
                    tr_variant* args;
                    tr_variant* top = tr_new0(tr_variant, 1);
                    tr_variantInitDict(top, 2);
                    tr_variantDictAddStr(top, TR_KEY_method, "torrent-set-location");
                    args = tr_variantDictAddDict(top, ARGUMENTS, 3);
                    tr_variantDictAddStr(args, TR_KEY_location, optarg);
                    tr_variantDictAddBool(args, TR_KEY_move, true);
                    addIdArg(args, id, NULL);
                    status |= flush(rpcurl, &top);
                    break;
                }

            default:
                {
                    fprintf(stderr, "got opt [%d]\n", c);
                    showUsage();
                    break;
                }
            }
        }
    }

    if (tadd != NULL)
    {
        status |= flush(rpcurl, &tadd);
    }

    if (tset != NULL)
    {
        addIdArg(tr_variantDictFind(tset, ARGUMENTS), id, NULL);
        status |= flush(rpcurl, &tset);
    }

    if (sset != NULL)
    {
        status |= flush(rpcurl, &sset);
    }

    return status;
}

/* [host:port] or [host] or [port] or [http(s?)://host:port/transmission/] */
static void getHostAndPortAndRpcUrl(int* argc, char** argv, char** host, int* port, char** rpcurl)
{
    if (*argv[1] == '-')
    {
        return;
    }

    char const* const s = argv[1];

    if (strncmp(s, "http://", 7) == 0) /* user passed in http rpc url */
    {
        *rpcurl = tr_strdup_printf("%s/rpc/", s + 7);
    }
    else if (strncmp(s, "https://", 8) == 0) /* user passed in https rpc url */
    {
        UseSSL = true;
        *rpcurl = tr_strdup_printf("%s/rpc/", s + 8);
    }
    else
    {
        char const* const first_colon = strchr(s, ':');
        char const* const last_colon = strrchr(s, ':');

        if (last_colon != NULL && ((*s == '[' && *(last_colon - 1) == ']') || first_colon == last_colon))
        {
            /* user passed in both host and port */
            *host = tr_strndup(s, last_colon - s);
            *port = atoi(last_colon + 1);
        }
        else
        {
            char* end;
            int const i = strtol(s, &end, 10);

            if (*end == '\0') /* user passed in a port */
            {
                *port = i;
            }
            else /* user passed in a host */
            {
                if (last_colon != NULL && first_colon != last_colon && (*s != '[' || *(s + strlen(s) - 1) != ']'))
                {
                    /* looks like IPv6; let's add brackets as we'll be appending the port later on */
                    *host = tr_strdup_printf("[%s]", s);
                }
                else
                {
                    *host = tr_strdup(s);
                }
            }
        }
    }

    *argc -= 1;

    for (int i = 1; i < *argc; ++i)
    {
        argv[i] = argv[i + 1];
    }
}

int tr_main(int argc, char* argv[])
{
    int port = DEFAULT_PORT;
    char* host = NULL;
    char* rpcurl = NULL;
    int exit_status = EXIT_SUCCESS;

    if (argc < 2)
    {
        showUsage();
        return EXIT_FAILURE;
    }

    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);

    getHostAndPortAndRpcUrl(&argc, argv, &host, &port, &rpcurl);

    if (host == NULL)
    {
        host = tr_strdup(DEFAULT_HOST);
    }

    if (rpcurl == NULL)
    {
        rpcurl = tr_strdup_printf("%s:%d%s", host, port, DEFAULT_URL);
    }

    exit_status = processArgs(rpcurl, argc, (char const* const*)argv);

    tr_free(host);
    tr_free(rpcurl);
    return exit_status;
}
