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

#include <stdlib.h> /* qsort() */

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

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

#include "favicon.h" /* gtr_get_favicon() */
#include "filter.h"
#include "hig.h" /* GUI_PAD */
#include "tr-core.h" /* MC_TORRENT */
#include "util.h" /* gtr_get_host_from_url() */

static GQuark DIRTY_KEY = 0;
static GQuark SESSION_KEY = 0;
static GQuark TEXT_KEY = 0;
static GQuark TORRENT_MODEL_KEY = 0;

/***
****
****  TRACKERS
****
***/

enum
{
    TRACKER_FILTER_TYPE_ALL,
    TRACKER_FILTER_TYPE_HOST,
    TRACKER_FILTER_TYPE_SEPARATOR,
};

enum
{
    TRACKER_FILTER_COL_NAME, /* human-readable name; ie, Legaltorrents */
    TRACKER_FILTER_COL_COUNT, /* how many matches there are */
    TRACKER_FILTER_COL_TYPE,
    TRACKER_FILTER_COL_HOST, /* pattern-matching text; ie, legaltorrents.com */
    TRACKER_FILTER_COL_PIXBUF,
    TRACKER_FILTER_N_COLS
};

static int pstrcmp(void const* a, void const* b)
{
    return g_strcmp0(*(char const* const*)a, *(char const* const*)b);
}

/* human-readable name; ie, Legaltorrents */
static char* get_name_from_host(char const* host)
{
    char* name;
    char const* dot = strrchr(host, '.');

    if (tr_addressIsIP(host))
    {
        name = g_strdup(host);
    }
    else if (dot != NULL)
    {
        name = g_strndup(host, dot - host);
    }
    else
    {
        name = g_strdup(host);
    }

    *name = g_ascii_toupper(*name);

    return name;
}

static void tracker_model_update_count(GtkTreeStore* store, GtkTreeIter* iter, int n)
{
    int count;
    GtkTreeModel* model = GTK_TREE_MODEL(store);
    gtk_tree_model_get(model, iter, TRACKER_FILTER_COL_COUNT, &count, -1);

    if (n != count)
    {
        gtk_tree_store_set(store, iter, TRACKER_FILTER_COL_COUNT, n, -1);
    }
}

static void favicon_ready_cb(gpointer pixbuf, gpointer vreference)
{
    GtkTreeIter iter;
    GtkTreeRowReference* reference = vreference;

    if (pixbuf != NULL)
    {
        GtkTreePath* path = gtk_tree_row_reference_get_path(reference);
        GtkTreeModel* model = gtk_tree_row_reference_get_model(reference);

        if (gtk_tree_model_get_iter(model, &iter, path))
        {
            gtk_tree_store_set(GTK_TREE_STORE(model), &iter, TRACKER_FILTER_COL_PIXBUF, pixbuf, -1);
        }

        gtk_tree_path_free(path);

        g_object_unref(pixbuf);
    }

    gtk_tree_row_reference_free(reference);
}

static gboolean tracker_filter_model_update(gpointer gstore)
{
    int all = 0;
    int store_pos;
    GtkTreeIter iter;
    GObject* o = G_OBJECT(gstore);
    GtkTreeStore* store = GTK_TREE_STORE(gstore);
    GtkTreeModel* model = GTK_TREE_MODEL(gstore);
    GPtrArray* hosts = g_ptr_array_new();
    GStringChunk* strings = g_string_chunk_new(4096);
    GHashTable* hosts_hash = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
    GtkTreeModel* tmodel = GTK_TREE_MODEL(g_object_get_qdata(o, TORRENT_MODEL_KEY));
    int const first_tracker_pos = 2; /* offset past the "All" and the separator */

    g_object_steal_qdata(o, DIRTY_KEY);

    /* Walk through all the torrents, tallying how many matches there are
     * for the various categories. Also make a sorted list of all tracker
     * hosts s.t. we can merge it with the existing list */
    if (gtk_tree_model_iter_nth_child(tmodel, &iter, NULL, 0))
    {
        do
        {
            tr_torrent* tor;
            tr_info const* inf;
            int keyCount;
            char** keys;

            gtk_tree_model_get(tmodel, &iter, MC_TORRENT, &tor, -1);
            inf = tr_torrentInfo(tor);
            keyCount = 0;
            keys = g_new(char*, inf->trackerCount);

            for (unsigned int i = 0; i < inf->trackerCount; ++i)
            {
                int* count;
                char buf[1024];
                char* key;

                gtr_get_host_from_url(buf, sizeof(buf), inf->trackers[i].announce);
                key = g_string_chunk_insert_const(strings, buf);

                count = g_hash_table_lookup(hosts_hash, key);

                if (count == NULL)
                {
                    count = tr_new0(int, 1);
                    g_hash_table_insert(hosts_hash, key, count);
                    g_ptr_array_add(hosts, key);
                }

                bool found = false;

                for (int k = 0; !found && k < keyCount; ++k)
                {
                    found = g_strcmp0(keys[k], key) == 0;
                }

                if (!found)
                {
                    keys[keyCount++] = key;
                }
            }

            for (int i = 0; i < keyCount; ++i)
            {
                int* incrementme = g_hash_table_lookup(hosts_hash, keys[i]);
                ++*incrementme;
            }

            g_free(keys);

            ++all;
        }
        while (gtk_tree_model_iter_next(tmodel, &iter));
    }

    qsort(hosts->pdata, hosts->len, sizeof(char*), pstrcmp);

    /* update the "all" count */
    if (gtk_tree_model_iter_children(model, &iter, NULL))
    {
        tracker_model_update_count(store, &iter, all);
    }

    store_pos = first_tracker_pos;

    for (int i = 0, n = hosts->len;;)
    {
        gboolean const new_hosts_done = i >= n;
        gboolean const old_hosts_done = !gtk_tree_model_iter_nth_child(model, &iter, NULL, store_pos);
        gboolean remove_row = FALSE;
        gboolean insert_row = FALSE;

        /* are we done yet? */
        if (new_hosts_done && old_hosts_done)
        {
            break;
        }

        /* decide what to do */
        if (new_hosts_done)
        {
            remove_row = TRUE;
        }
        else if (old_hosts_done)
        {
            insert_row = TRUE;
        }
        else
        {
            int cmp;
            char* host;
            gtk_tree_model_get(model, &iter, TRACKER_FILTER_COL_HOST, &host, -1);
            cmp = g_strcmp0(host, hosts->pdata[i]);

            if (cmp < 0)
            {
                remove_row = TRUE;
            }
            else if (cmp > 0)
            {
                insert_row = TRUE;
            }

            g_free(host);
        }

        /* do something */
        if (remove_row)
        {
            /* g_message ("removing row and incrementing i"); */
            gtk_tree_store_remove(store, &iter);
        }
        else if (insert_row)
        {
            GtkTreeIter add;
            GtkTreePath* path;
            GtkTreeRowReference* reference;
            tr_session* session = g_object_get_qdata(G_OBJECT(store), SESSION_KEY);
            char const* host = hosts->pdata[i];
            char* name = get_name_from_host(host);
            int const count = *(int*)g_hash_table_lookup(hosts_hash, host);
            gtk_tree_store_insert_with_values(store, &add, NULL, store_pos,
                TRACKER_FILTER_COL_HOST, host,
                TRACKER_FILTER_COL_NAME, name,
                TRACKER_FILTER_COL_COUNT, count,
                TRACKER_FILTER_COL_TYPE, TRACKER_FILTER_TYPE_HOST,
                -1);
            path = gtk_tree_model_get_path(model, &add);
            reference = gtk_tree_row_reference_new(model, path);
            gtr_get_favicon(session, host, favicon_ready_cb, reference);
            gtk_tree_path_free(path);
            g_free(name);
            ++store_pos;
            ++i;
        }
        else /* update row */
        {
            char const* host = hosts->pdata[i];
            int const count = *(int*)g_hash_table_lookup(hosts_hash, host);
            tracker_model_update_count(store, &iter, count);
            ++store_pos;
            ++i;
        }
    }

    /* cleanup */
    g_ptr_array_free(hosts, TRUE);
    g_hash_table_unref(hosts_hash);
    g_string_chunk_free(strings);
    return G_SOURCE_REMOVE;
}

static GtkTreeModel* tracker_filter_model_new(GtkTreeModel* tmodel)
{
    GtkTreeStore* store = gtk_tree_store_new(TRACKER_FILTER_N_COLS,
        G_TYPE_STRING,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_STRING,
        GDK_TYPE_PIXBUF);

    gtk_tree_store_insert_with_values(store, NULL, NULL, -1,
        TRACKER_FILTER_COL_NAME, _("All"),
        TRACKER_FILTER_COL_TYPE, TRACKER_FILTER_TYPE_ALL,
        -1);
    gtk_tree_store_insert_with_values(store, NULL, NULL, -1,
        TRACKER_FILTER_COL_TYPE, TRACKER_FILTER_TYPE_SEPARATOR,
        -1);

    g_object_set_qdata(G_OBJECT(store), TORRENT_MODEL_KEY, tmodel);
    tracker_filter_model_update(store);
    return GTK_TREE_MODEL(store);
}

static gboolean is_it_a_separator(GtkTreeModel* m, GtkTreeIter* iter, gpointer data UNUSED)
{
    int type;
    gtk_tree_model_get(m, iter, TRACKER_FILTER_COL_TYPE, &type, -1);
    return type == TRACKER_FILTER_TYPE_SEPARATOR;
}

static void tracker_model_update_idle(gpointer tracker_model)
{
    GObject* o = G_OBJECT(tracker_model);
    gboolean const pending = g_object_get_qdata(o, DIRTY_KEY) != NULL;

    if (!pending)
    {
        GSourceFunc func = tracker_filter_model_update;
        g_object_set_qdata(o, DIRTY_KEY, GINT_TO_POINTER(1));
        gdk_threads_add_idle(func, tracker_model);
    }
}

static void torrent_model_row_changed(GtkTreeModel* tmodel UNUSED, GtkTreePath* path UNUSED, GtkTreeIter* iter UNUSED,
    gpointer tracker_model)
{
    tracker_model_update_idle(tracker_model);
}

static void torrent_model_row_deleted_cb(GtkTreeModel* tmodel UNUSED, GtkTreePath* path UNUSED, gpointer tracker_model)
{
    tracker_model_update_idle(tracker_model);
}

static void render_pixbuf_func(GtkCellLayout* cell_layout UNUSED, GtkCellRenderer* cell_renderer, GtkTreeModel* tree_model,
    GtkTreeIter* iter, gpointer data UNUSED)
{
    int type;
    int width;

    gtk_tree_model_get(tree_model, iter, TRACKER_FILTER_COL_TYPE, &type, -1);
    width = (type == TRACKER_FILTER_TYPE_HOST) ? 20 : 0;
    g_object_set(cell_renderer, "width", width, NULL);
}

static void render_number_func(GtkCellLayout* cell_layout UNUSED, GtkCellRenderer* cell_renderer, GtkTreeModel* tree_model,
    GtkTreeIter* iter, gpointer data UNUSED)
{
    int count;
    char buf[32];

    gtk_tree_model_get(tree_model, iter, TRACKER_FILTER_COL_COUNT, &count, -1);

    if (count >= 0)
    {
        g_snprintf(buf, sizeof(buf), "%'d", count);
    }
    else
    {
        *buf = '\0';
    }

    g_object_set(cell_renderer, "text", buf, NULL);
}

static GtkCellRenderer* number_renderer_new(void)
{
    GtkCellRenderer* r = gtk_cell_renderer_text_new();

    g_object_set(G_OBJECT(r), "alignment", PANGO_ALIGN_RIGHT, "weight", PANGO_WEIGHT_ULTRALIGHT, "xalign", 1.0, "xpad", GUI_PAD,
        NULL);

    return r;
}

static void disconnect_cat_model_callbacks(gpointer tmodel, GObject* cat_model)
{
    g_signal_handlers_disconnect_by_func(tmodel, torrent_model_row_changed, cat_model);
    g_signal_handlers_disconnect_by_func(tmodel, torrent_model_row_deleted_cb, cat_model);
}

static GtkWidget* tracker_combo_box_new(GtkTreeModel* tmodel)
{
    GtkWidget* c;
    GtkCellRenderer* r;
    GtkTreeModel* cat_model;
    GtkCellLayout* c_cell_layout;
    GtkComboBox* c_combo_box;

    /* create the tracker combobox */
    cat_model = tracker_filter_model_new(tmodel);
    c = gtk_combo_box_new_with_model(cat_model);
    c_combo_box = GTK_COMBO_BOX(c);
    c_cell_layout = GTK_CELL_LAYOUT(c);
    gtk_combo_box_set_row_separator_func(c_combo_box, is_it_a_separator, NULL, NULL);
    gtk_combo_box_set_active(c_combo_box, 0);

    r = gtk_cell_renderer_pixbuf_new();
    gtk_cell_layout_pack_start(c_cell_layout, r, FALSE);
    gtk_cell_layout_set_cell_data_func(c_cell_layout, r, render_pixbuf_func, NULL, NULL);
    gtk_cell_layout_set_attributes(c_cell_layout, r, "pixbuf", TRACKER_FILTER_COL_PIXBUF, NULL);

    r = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(c_cell_layout, r, FALSE);
    gtk_cell_layout_set_attributes(c_cell_layout, r, "text", TRACKER_FILTER_COL_NAME, NULL);

    r = number_renderer_new();
    gtk_cell_layout_pack_end(c_cell_layout, r, TRUE);
    gtk_cell_layout_set_cell_data_func(c_cell_layout, r, render_number_func, NULL, NULL);

    g_object_weak_ref(G_OBJECT(cat_model), disconnect_cat_model_callbacks, tmodel);
    g_signal_connect(tmodel, "row-changed", G_CALLBACK(torrent_model_row_changed), cat_model);
    g_signal_connect(tmodel, "row-inserted", G_CALLBACK(torrent_model_row_changed), cat_model);
    g_signal_connect(tmodel, "row-deleted", G_CALLBACK(torrent_model_row_deleted_cb), cat_model);

    return c;
}

static gboolean test_tracker(tr_torrent* tor, int active_tracker_type, char const* host)
{
    gboolean matches = TRUE;

    if (active_tracker_type == TRACKER_FILTER_TYPE_HOST)
    {
        char tmp[1024];
        tr_info const* const inf = tr_torrentInfo(tor);

        matches = FALSE;

        for (unsigned int i = 0; !matches && i < inf->trackerCount; ++i)
        {
            gtr_get_host_from_url(tmp, sizeof(tmp), inf->trackers[i].announce);
            matches = g_strcmp0(tmp, host) == 0;
        }
    }

    return matches;
}

/***
****
****  ACTIVITY
****
***/

enum
{
    ACTIVITY_FILTER_ALL,
    ACTIVITY_FILTER_DOWNLOADING,
    ACTIVITY_FILTER_SEEDING,
    ACTIVITY_FILTER_ACTIVE,
    ACTIVITY_FILTER_PAUSED,
    ACTIVITY_FILTER_FINISHED,
    ACTIVITY_FILTER_VERIFYING,
    ACTIVITY_FILTER_ERROR,
    ACTIVITY_FILTER_SEPARATOR
};

enum
{
    ACTIVITY_FILTER_COL_NAME,
    ACTIVITY_FILTER_COL_COUNT,
    ACTIVITY_FILTER_COL_TYPE,
    ACTIVITY_FILTER_COL_STOCK_ID,
    ACTIVITY_FILTER_N_COLS
};

static gboolean activity_is_it_a_separator(GtkTreeModel* m, GtkTreeIter* i, gpointer d UNUSED)
{
    int type;
    gtk_tree_model_get(m, i, ACTIVITY_FILTER_COL_TYPE, &type, -1);
    return type == ACTIVITY_FILTER_SEPARATOR;
}

static gboolean test_torrent_activity(tr_torrent* tor, int type)
{
    tr_stat const* st = tr_torrentStatCached(tor);

    switch (type)
    {
    case ACTIVITY_FILTER_DOWNLOADING:
        return st->activity == TR_STATUS_DOWNLOAD || st->activity == TR_STATUS_DOWNLOAD_WAIT;

    case ACTIVITY_FILTER_SEEDING:
        return st->activity == TR_STATUS_SEED || st->activity == TR_STATUS_SEED_WAIT;

    case ACTIVITY_FILTER_ACTIVE:
        return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0 || st->webseedsSendingToUs > 0 ||
            st->activity == TR_STATUS_CHECK;

    case ACTIVITY_FILTER_PAUSED:
        return st->activity == TR_STATUS_STOPPED;

    case ACTIVITY_FILTER_FINISHED:
        return st->finished == TRUE;

    case ACTIVITY_FILTER_VERIFYING:
        return st->activity == TR_STATUS_CHECK || st->activity == TR_STATUS_CHECK_WAIT;

    case ACTIVITY_FILTER_ERROR:
        return st->error != 0;

    default: /* ACTIVITY_FILTER_ALL */
        return TRUE;
    }
}

static void status_model_update_count(GtkListStore* store, GtkTreeIter* iter, int n)
{
    int count;
    GtkTreeModel* model = GTK_TREE_MODEL(store);
    gtk_tree_model_get(model, iter, ACTIVITY_FILTER_COL_COUNT, &count, -1);

    if (n != count)
    {
        gtk_list_store_set(store, iter, ACTIVITY_FILTER_COL_COUNT, n, -1);
    }
}

static gboolean activity_filter_model_update(gpointer gstore)
{
    GtkTreeIter iter;
    GObject* o = G_OBJECT(gstore);
    GtkListStore* store = GTK_LIST_STORE(gstore);
    GtkTreeModel* model = GTK_TREE_MODEL(store);
    GtkTreeModel* tmodel = GTK_TREE_MODEL(g_object_get_qdata(o, TORRENT_MODEL_KEY));

    g_object_steal_qdata(o, DIRTY_KEY);

    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        do
        {
            int hits;
            int type;
            GtkTreeIter torrent_iter;

            gtk_tree_model_get(model, &iter, ACTIVITY_FILTER_COL_TYPE, &type, -1);

            hits = 0;

            if (gtk_tree_model_iter_nth_child(tmodel, &torrent_iter, NULL, 0))
            {
                do
                {
                    tr_torrent* tor;
                    gtk_tree_model_get(tmodel, &torrent_iter, MC_TORRENT, &tor, -1);

                    if (test_torrent_activity(tor, type))
                    {
                        ++hits;
                    }
                }
                while (gtk_tree_model_iter_next(tmodel, &torrent_iter));
            }

            status_model_update_count(store, &iter, hits);
        }
        while (gtk_tree_model_iter_next(model, &iter));
    }

    return G_SOURCE_REMOVE;
}

static GtkTreeModel* activity_filter_model_new(GtkTreeModel* tmodel)
{
    struct
    {
        int type;
        char const* context;
        char const* name;
        char const* stock_id;
    }
    types[] =
    {
        { ACTIVITY_FILTER_ALL, NULL, N_("All"), NULL },
        { ACTIVITY_FILTER_SEPARATOR, NULL, NULL, NULL },
        { ACTIVITY_FILTER_ACTIVE, NULL, N_("Active"), GTK_STOCK_EXECUTE },
        { ACTIVITY_FILTER_DOWNLOADING, "Verb", NC_("Verb", "Downloading"), GTK_STOCK_GO_DOWN },
        { ACTIVITY_FILTER_SEEDING, "Verb", NC_("Verb", "Seeding"), GTK_STOCK_GO_UP },
        { ACTIVITY_FILTER_PAUSED, NULL, N_("Paused"), GTK_STOCK_MEDIA_PAUSE },
        { ACTIVITY_FILTER_FINISHED, NULL, N_("Finished"), NULL },
        { ACTIVITY_FILTER_VERIFYING, "Verb", NC_("Verb", "Verifying"), GTK_STOCK_REFRESH },
        { ACTIVITY_FILTER_ERROR, NULL, N_("Error"), GTK_STOCK_DIALOG_ERROR }
    };

    GtkListStore* store = gtk_list_store_new(ACTIVITY_FILTER_N_COLS,
        G_TYPE_STRING,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_STRING);

    for (size_t i = 0; i < G_N_ELEMENTS(types); ++i)
    {
        char const* name = types[i].context != NULL ? g_dpgettext2(NULL, types[i].context, types[i].name) : _(types[i].name);
        gtk_list_store_insert_with_values(store, NULL, -1,
            ACTIVITY_FILTER_COL_NAME, name,
            ACTIVITY_FILTER_COL_TYPE, types[i].type,
            ACTIVITY_FILTER_COL_STOCK_ID, types[i].stock_id,
            -1);
    }

    g_object_set_qdata(G_OBJECT(store), TORRENT_MODEL_KEY, tmodel);
    activity_filter_model_update(store);
    return GTK_TREE_MODEL(store);
}

static void render_activity_pixbuf_func(GtkCellLayout* cell_layout UNUSED, GtkCellRenderer* cell_renderer,
    GtkTreeModel* tree_model, GtkTreeIter* iter, gpointer data UNUSED)
{
    int type;
    int width;
    int ypad;

    gtk_tree_model_get(tree_model, iter, ACTIVITY_FILTER_COL_TYPE, &type, -1);
    width = type == ACTIVITY_FILTER_ALL ? 0 : 20;
    ypad = type == ACTIVITY_FILTER_ALL ? 0 : 2;

    g_object_set(cell_renderer, "width", width, "ypad", ypad, NULL);
}

static void activity_model_update_idle(gpointer activity_model)
{
    GObject* o = G_OBJECT(activity_model);
    gboolean const pending = g_object_get_qdata(o, DIRTY_KEY) != NULL;

    if (!pending)
    {
        GSourceFunc func = activity_filter_model_update;
        g_object_set_qdata(o, DIRTY_KEY, GINT_TO_POINTER(1));
        gdk_threads_add_idle(func, activity_model);
    }
}

static void activity_torrent_model_row_changed(GtkTreeModel* tmodel UNUSED, GtkTreePath* path UNUSED, GtkTreeIter* iter UNUSED,
    gpointer activity_model)
{
    activity_model_update_idle(activity_model);
}

static void activity_torrent_model_row_deleted_cb(GtkTreeModel* tmodel UNUSED, GtkTreePath* path UNUSED,
    gpointer activity_model)
{
    activity_model_update_idle(activity_model);
}

static void disconnect_activity_model_callbacks(gpointer tmodel, GObject* cat_model)
{
    g_signal_handlers_disconnect_by_func(tmodel, activity_torrent_model_row_changed, cat_model);
    g_signal_handlers_disconnect_by_func(tmodel, activity_torrent_model_row_deleted_cb, cat_model);
}

static GtkWidget* activity_combo_box_new(GtkTreeModel* tmodel)
{
    GtkWidget* c;
    GtkCellRenderer* r;
    GtkTreeModel* activity_model;
    GtkComboBox* c_combo_box;
    GtkCellLayout* c_cell_layout;

    activity_model = activity_filter_model_new(tmodel);
    c = gtk_combo_box_new_with_model(activity_model);
    c_combo_box = GTK_COMBO_BOX(c);
    c_cell_layout = GTK_CELL_LAYOUT(c);
    gtk_combo_box_set_row_separator_func(c_combo_box, activity_is_it_a_separator, NULL, NULL);
    gtk_combo_box_set_active(c_combo_box, 0);

    r = gtk_cell_renderer_pixbuf_new();
    gtk_cell_layout_pack_start(c_cell_layout, r, FALSE);
    gtk_cell_layout_set_attributes(c_cell_layout, r, "stock-id", ACTIVITY_FILTER_COL_STOCK_ID, NULL);
    gtk_cell_layout_set_cell_data_func(c_cell_layout, r, render_activity_pixbuf_func, NULL, NULL);

    r = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(c_cell_layout, r, TRUE);
    gtk_cell_layout_set_attributes(c_cell_layout, r, "text", ACTIVITY_FILTER_COL_NAME, NULL);

    r = number_renderer_new();
    gtk_cell_layout_pack_end(c_cell_layout, r, TRUE);
    gtk_cell_layout_set_cell_data_func(c_cell_layout, r, render_number_func, NULL, NULL);

    g_object_weak_ref(G_OBJECT(activity_model), disconnect_activity_model_callbacks, tmodel);
    g_signal_connect(tmodel, "row-changed", G_CALLBACK(activity_torrent_model_row_changed), activity_model);
    g_signal_connect(tmodel, "row-inserted", G_CALLBACK(activity_torrent_model_row_changed), activity_model);
    g_signal_connect(tmodel, "row-deleted", G_CALLBACK(activity_torrent_model_row_deleted_cb), activity_model);

    return c;
}

/****
*****
*****  ENTRY FIELD
*****
****/

static gboolean testText(tr_torrent const* tor, char const* key)
{
    gboolean ret = FALSE;

    if (tr_str_is_empty(key))
    {
        ret = TRUE;
    }
    else
    {
        tr_info const* inf = tr_torrentInfo(tor);

        /* test the torrent name... */
        {
            char* pch = g_utf8_casefold(tr_torrentName(tor), -1);
            ret = key == NULL || strstr(pch, key) != NULL;
            g_free(pch);
        }

        /* test the files... */
        for (tr_file_index_t i = 0; i < inf->fileCount && !ret; ++i)
        {
            char* pch = g_utf8_casefold(inf->files[i].name, -1);
            ret = key == NULL || strstr(pch, key) != NULL;
            g_free(pch);
        }
    }

    return ret;
}

static void entry_clear(GtkEntry* e)
{
    gtk_entry_set_text(e, "");
}

static void filter_entry_changed(GtkEditable* e, gpointer filter_model)
{
    char* pch;
    char* folded;

    pch = gtk_editable_get_chars(e, 0, -1);
    folded = g_utf8_casefold(pch, -1);
    g_strstrip(folded);
    g_object_set_qdata_full(filter_model, TEXT_KEY, folded, g_free);
    g_free(pch);

    gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(filter_model));
}

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

struct filter_data
{
    GtkWidget* activity;
    GtkWidget* tracker;
    GtkWidget* entry;
    GtkWidget* show_lb;
    GtkTreeModel* filter_model;
    int active_activity_type;
    int active_tracker_type;
    char* active_tracker_host;
};

static gboolean is_row_visible(GtkTreeModel* model, GtkTreeIter* iter, gpointer vdata)
{
    char const* text;
    tr_torrent* tor;
    struct filter_data* data = vdata;
    GObject* o = G_OBJECT(data->filter_model);

    gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);

    text = (char const*)g_object_get_qdata(o, TEXT_KEY);

    return tor != NULL && test_tracker(tor, data->active_tracker_type, data->active_tracker_host) &&
        test_torrent_activity(tor, data->active_activity_type) && testText(tor, text);
}

static void selection_changed_cb(GtkComboBox* combo, gpointer vdata)
{
    int type;
    char* host;
    GtkTreeIter iter;
    GtkTreeModel* model;
    struct filter_data* data = vdata;

    /* set data->active_activity_type from the activity combobox */
    combo = GTK_COMBO_BOX(data->activity);
    model = gtk_combo_box_get_model(combo);

    if (gtk_combo_box_get_active_iter(combo, &iter))
    {
        gtk_tree_model_get(model, &iter, ACTIVITY_FILTER_COL_TYPE, &type, -1);
    }
    else
    {
        type = ACTIVITY_FILTER_ALL;
    }

    data->active_activity_type = type;

    /* set the active tracker type & host from the tracker combobox */
    combo = GTK_COMBO_BOX(data->tracker);
    model = gtk_combo_box_get_model(combo);

    if (gtk_combo_box_get_active_iter(combo, &iter))
    {
        gtk_tree_model_get(model, &iter,
            TRACKER_FILTER_COL_TYPE, &type,
            TRACKER_FILTER_COL_HOST, &host,
            -1);
    }
    else
    {
        type = TRACKER_FILTER_TYPE_ALL;
        host = NULL;
    }

    g_free(data->active_tracker_host);
    data->active_tracker_host = host;
    data->active_tracker_type = type;

    /* refilter */
    gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(data->filter_model));
}

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

static gboolean update_count_label(gpointer gdata)
{
    char buf[512];
    int visibleCount;
    int trackerCount;
    int activityCount;
    GtkTreeModel* model;
    GtkComboBox* combo;
    GtkTreeIter iter;
    struct filter_data* data = gdata;

    /* get the visible count */
    visibleCount = gtk_tree_model_iter_n_children(data->filter_model, NULL);

    /* get the tracker count */
    combo = GTK_COMBO_BOX(data->tracker);
    model = gtk_combo_box_get_model(combo);

    if (gtk_combo_box_get_active_iter(combo, &iter))
    {
        gtk_tree_model_get(model, &iter, TRACKER_FILTER_COL_COUNT, &trackerCount, -1);
    }
    else
    {
        trackerCount = 0;
    }

    /* get the activity count */
    combo = GTK_COMBO_BOX(data->activity);
    model = gtk_combo_box_get_model(combo);

    if (gtk_combo_box_get_active_iter(combo, &iter))
    {
        gtk_tree_model_get(model, &iter, ACTIVITY_FILTER_COL_COUNT, &activityCount, -1);
    }
    else
    {
        activityCount = 0;
    }

    /* set the text */
    if (visibleCount == MIN(activityCount, trackerCount))
    {
        g_snprintf(buf, sizeof(buf), _("_Show:"));
    }
    else
    {
        g_snprintf(buf, sizeof(buf), _("_Show %'d of:"), visibleCount);
    }

    gtk_label_set_markup_with_mnemonic(GTK_LABEL(data->show_lb), buf);

    g_object_steal_qdata(G_OBJECT(data->show_lb), DIRTY_KEY);
    return G_SOURCE_REMOVE;
}

static void update_count_label_idle(struct filter_data* data)
{
    GObject* o = G_OBJECT(data->show_lb);
    gboolean const pending = g_object_get_qdata(o, DIRTY_KEY) != NULL;

    if (!pending)
    {
        g_object_set_qdata(o, DIRTY_KEY, GINT_TO_POINTER(1));
        gdk_threads_add_idle(update_count_label, data);
    }
}

static void on_filter_model_row_inserted(GtkTreeModel* tree_model UNUSED, GtkTreePath* path UNUSED, GtkTreeIter* iter UNUSED,
    gpointer data)
{
    update_count_label_idle(data);
}

static void on_filter_model_row_deleted(GtkTreeModel* tree_model UNUSED, GtkTreePath* path UNUSED, gpointer data)
{
    update_count_label_idle(data);
}

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

GtkWidget* gtr_filter_bar_new(tr_session* session, GtkTreeModel* tmodel, GtkTreeModel** filter_model)
{
    GtkWidget* l;
    GtkWidget* w;
    GtkWidget* h;
    GtkWidget* s;
    GtkWidget* activity;
    GtkWidget* tracker;
    GtkBox* h_box;
    struct filter_data* data;

    g_assert(DIRTY_KEY == 0);
    TEXT_KEY = g_quark_from_static_string("tr-filter-text-key");
    DIRTY_KEY = g_quark_from_static_string("tr-filter-dirty-key");
    SESSION_KEY = g_quark_from_static_string("tr-session-key");
    TORRENT_MODEL_KEY = g_quark_from_static_string("tr-filter-torrent-model-key");

    data = g_new0(struct filter_data, 1);
    data->show_lb = gtk_label_new(NULL);
    data->activity = activity = activity_combo_box_new(tmodel);
    data->tracker = tracker = tracker_combo_box_new(tmodel);
    data->filter_model = gtk_tree_model_filter_new(tmodel, NULL);
    g_signal_connect(data->filter_model, "row-deleted", G_CALLBACK(on_filter_model_row_deleted), data);
    g_signal_connect(data->filter_model, "row-inserted", G_CALLBACK(on_filter_model_row_inserted), data);

    g_object_set(G_OBJECT(data->tracker), "width-request", 170, NULL);
    g_object_set_qdata(G_OBJECT(gtk_combo_box_get_model(GTK_COMBO_BOX(data->tracker))), SESSION_KEY, session);

    gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(data->filter_model), is_row_visible, data, g_free);

    g_signal_connect(data->tracker, "changed", G_CALLBACK(selection_changed_cb), data);
    g_signal_connect(data->activity, "changed", G_CALLBACK(selection_changed_cb), data);

    h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_SMALL);
    h_box = GTK_BOX(h);

    /* add the activity combobox */
    w = activity;
    l = data->show_lb;
    gtk_label_set_mnemonic_widget(GTK_LABEL(l), w);
    gtk_box_pack_start(h_box, l, FALSE, FALSE, 0);
    gtk_box_pack_start(h_box, w, TRUE, TRUE, 0);
#if GTK_CHECK_VERSION(3, 12, 0)
    gtk_widget_set_margin_end(w, GUI_PAD);
#else
    gtk_widget_set_margin_right(w, GUI_PAD);
#endif

    /* add the tracker combobox */
    w = tracker;
    gtk_box_pack_start(h_box, w, TRUE, TRUE, 0);
#if GTK_CHECK_VERSION(3, 12, 0)
    gtk_widget_set_margin_end(w, GUI_PAD);
#else
    gtk_widget_set_margin_right(w, GUI_PAD);
#endif

    /* add the entry field */
    s = gtk_entry_new();
    gtk_entry_set_icon_from_stock(GTK_ENTRY(s), GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLEAR);
    g_signal_connect(s, "icon-release", G_CALLBACK(entry_clear), NULL);
    gtk_box_pack_start(h_box, s, TRUE, TRUE, 0);

    g_signal_connect(s, "changed", G_CALLBACK(filter_entry_changed), data->filter_model);
    selection_changed_cb(NULL, data);

    *filter_model = data->filter_model;
    update_count_label(data);
    return h;
}
