/*
 * Copyright © 2004-2010 Jens Oknelid, paskharen@gmail.com
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, compiling, linking, and/or
 * using OpenSSL with this program is allowed.
 */

#include "search.hh"
#include <glib.h>
#include <dcpp/FavoriteManager.h>
#include <dcpp/QueueManager.h>
#include <dcpp/ShareManager.h>
#include <dcpp/StringTokenizer.h>
#include <dcpp/Text.h>
#include <dcpp/UserCommand.h>
#include "UserCommandMenu.hh"
#include "wulformanager.hh"
#include "WulforUtil.hh"
#include "settingsmanager.hh"

using namespace std;
using namespace dcpp;

GtkTreeModel* Search::searchEntriesModel = NULL;

Search::Search():
    BookEntry(Entry::SEARCH, _("Search"), "search.ui", generateID(this)),
    previousGrouping(NOGROUPING)
{
#if GTK_CHECK_VERSION(3,0,0)
    gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(getWidget("progressbar1")), true);
#else
    gtk_statusbar_set_has_resize_grip (GTK_STATUSBAR(getWidget("statusbar2")),false);
    gtk_statusbar_set_has_resize_grip (GTK_STATUSBAR(getWidget("statusbar3")),false);
#endif

    // Initialize variables for search progressbar
    stop = true;
    waitingResults = false;
    searchStartTime = 0;
    searchEndTime = 1;
    setProgress_gui("progressbar1", "", 0.0);
    gtk_widget_hide(getWidget("statusbox"));

    // Initialize the search entries combo box
    if(!searchEntriesModel)
        searchEntriesModel = gtk_combo_box_get_model(GTK_COMBO_BOX(getWidget("comboboxentrySearch")));
    gtk_combo_box_set_model(GTK_COMBO_BOX(getWidget("comboboxentrySearch")), searchEntriesModel);
    searchEntry = gtk_bin_get_child(GTK_BIN(getWidget("comboboxentrySearch")));
    gtk_widget_grab_focus(getWidget("comboboxentrySearch"));

    // Configure the dialog
    File::ensureDirectory(SETTING(DOWNLOAD_DIRECTORY));
    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(getWidget("dirChooserDialog")), Text::fromUtf8(SETTING(DOWNLOAD_DIRECTORY)).c_str());
    gtk_dialog_set_alternative_button_order(GTK_DIALOG(getWidget("dirChooserDialog")), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1);

    // menu
    g_object_ref_sink(getWidget("mainMenu"));

    // Initialize check button options.
    onlyFree = BOOLSETTING(SEARCH_ONLY_FREE_SLOTS);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(getWidget("checkbuttonSlots")), onlyFree);
    gtk_widget_set_sensitive(GTK_WIDGET(getWidget("checkbuttonSlots")), false);
    gtk_widget_set_sensitive(GTK_WIDGET(getWidget("checkbuttonShared")), false);

    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxSize")), 1);
    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxUnit")), 2);
    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxFile")), 0);
    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")), (int)TTH);

    // Initialize hub list treeview
    hubView.setView(GTK_TREE_VIEW(getWidget("treeviewHubs")));
    hubView.insertColumn(_("Search"), G_TYPE_BOOLEAN, TreeView::BOOL, -1);
    hubView.insertColumn(_("Name"), G_TYPE_STRING, TreeView::STRING, -1);
    hubView.insertHiddenColumn("Url", G_TYPE_STRING);
    hubView.finalize();
    hubStore = gtk_list_store_newv(hubView.getColCount(), hubView.getGTypes());
    gtk_tree_view_set_model(hubView.get(), GTK_TREE_MODEL(hubStore));
    g_object_unref(hubStore);
    GtkTreeViewColumn *col = gtk_tree_view_get_column(hubView.get(), hubView.col(_("Search")));
    GList *list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(col));
    GtkCellRenderer *renderer = (GtkCellRenderer *)g_list_nth_data(list, 0);
    g_list_free(list);

    // Initialize search result treeview
    resultView.setView(GTK_TREE_VIEW(getWidget("treeviewResult")), true, "search");
    resultView.insertColumn(_("Filename"), G_TYPE_STRING, TreeView::ICON_STRING, 250, "Icon");
    resultView.insertColumn(_("Nick"), G_TYPE_STRING, TreeView::STRING, 100);
    resultView.insertColumn(_("Type"), G_TYPE_STRING, TreeView::STRING, 65);
    resultView.insertColumn(_("Size"), G_TYPE_STRING, TreeView::STRING, 80);
    resultView.insertColumn(_("Path"), G_TYPE_STRING, TreeView::STRING, 100);
    resultView.insertColumn(_("Slots"), G_TYPE_STRING, TreeView::STRING, 50);
    resultView.insertColumn(_("Connection"), G_TYPE_STRING, TreeView::STRING, 90);
    resultView.insertColumn(_("Hub"), G_TYPE_STRING, TreeView::STRING, 150);
    resultView.insertColumn(_("Exact Size"), G_TYPE_STRING, TreeView::STRING, 80);
    resultView.insertColumn(_("IP"), G_TYPE_STRING, TreeView::STRING, 100);
    resultView.insertColumn(_("TTH"), G_TYPE_STRING, TreeView::STRING, 125);
    resultView.insertHiddenColumn("Icon", G_TYPE_STRING);
    resultView.insertHiddenColumn("Real Size", G_TYPE_INT64);
    resultView.insertHiddenColumn("Slots Order", G_TYPE_INT);
    resultView.insertHiddenColumn("File Order", G_TYPE_STRING);
    resultView.insertHiddenColumn("Hub URL", G_TYPE_STRING);
    resultView.insertHiddenColumn("CID", G_TYPE_STRING);
    resultView.insertHiddenColumn("Shared", G_TYPE_BOOLEAN);
    resultView.insertHiddenColumn("Free Slots", G_TYPE_INT);
    resultView.finalize();
    resultStore = gtk_tree_store_newv(resultView.getColCount(), resultView.getGTypes());
    searchFilterModel = gtk_tree_model_filter_new(GTK_TREE_MODEL(resultStore), NULL);
    gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(searchFilterModel), &Search::searchFilterFunc_gui, (gpointer)this, NULL);
    sortedFilterModel = gtk_tree_model_sort_new_with_model(searchFilterModel);
    gtk_tree_view_set_model(resultView.get(), sortedFilterModel);
    g_object_unref(resultStore);
    g_object_unref(searchFilterModel);
    g_object_unref(sortedFilterModel);
    selection = gtk_tree_view_get_selection(resultView.get());
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
    resultView.setSortColumn_gui(_("Size"), "Real Size");
    resultView.setSortColumn_gui(_("Exact Size"), "Real Size");
    resultView.setSortColumn_gui(_("Slots"), "Slots Order");
    resultView.setSortColumn_gui(_("Filename"), "File Order");
    gtk_tree_view_set_fixed_height_mode(resultView.get(), true);

    // Initialize the user command menu
    userCommandMenu = new UserCommandMenu(getWidget("usercommandMenu"), ::UserCommand::CONTEXT_SEARCH);
    addChild(userCommandMenu);

    // Initialize search types
    GtkTreeIter iter;
    GtkComboBox *combo_box = GTK_COMBO_BOX(getWidget("comboboxFile"));
    GtkTreeModel *model = gtk_combo_box_get_model(combo_box);
    GtkListStore *store = GTK_LIST_STORE(model);
    const SettingsManager::SearchTypes &searchTypes = SettingsManager::getInstance()->getSearchTypes();

    // Predefined
    for (int i = SearchManager::TYPE_ANY; i < SearchManager::TYPE_LAST; ++i)
    {
        gtk_list_store_append(store, &iter);
        gtk_list_store_set(store, &iter, 0, SearchManager::getTypeStr(i), -1);
    }

    // Customs
    for (auto& i: searchTypes)
    {
        const string type = i.first;
        if (!(type.size() == 1 && type[0] >= '1' && type[0] <= '7'))
        {
            gtk_list_store_append(store, &iter);
            gtk_list_store_set(store, &iter, 0, type.c_str(), -1);
        }
    }
    gtk_combo_box_set_active(combo_box, WGETI("last-search-type"));

    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(getWidget("togglebuttonSidePanel")), true);

    // Connect the signals to their callback functions.
    g_signal_connect(getContainer(), "focus-in-event", G_CALLBACK(onFocusIn_gui), (gpointer)this);
    g_signal_connect(getWidget("checkbuttonFilter"), "toggled", G_CALLBACK(onFilterButtonToggled_gui), (gpointer)this);
    g_signal_connect(getWidget("checkbuttonSlots"), "toggled", G_CALLBACK(onSlotsButtonToggled_gui), (gpointer)this);
    g_signal_connect(getWidget("checkbuttonShared"), "toggled", G_CALLBACK(onSharedButtonToggled_gui), (gpointer)this);
    g_signal_connect(renderer, "toggled", G_CALLBACK(onToggledClicked_gui), (gpointer)this);
    g_signal_connect(resultView.get(), "button-press-event", G_CALLBACK(onButtonPressed_gui), (gpointer)this);
    g_signal_connect(resultView.get(), "button-release-event", G_CALLBACK(onButtonReleased_gui), (gpointer)this);
    g_signal_connect(resultView.get(), "key-release-event", G_CALLBACK(onKeyReleased_gui), (gpointer)this);
    g_signal_connect(searchEntry, "key-press-event", G_CALLBACK(onSearchEntryKeyPressed_gui), (gpointer)this);
    g_signal_connect(searchEntry, "key-release-event", G_CALLBACK(onKeyReleased_gui), (gpointer)this);
    g_signal_connect(getWidget("entrySize"), "key-press-event", G_CALLBACK(onSearchEntryKeyPressed_gui), (gpointer)this);
    g_signal_connect(getWidget("entrySize"), "key-release-event", G_CALLBACK(onKeyReleased_gui), (gpointer)this);
    g_signal_connect(getWidget("buttonSearch"), "clicked", G_CALLBACK(onSearchButtonClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("downloadItem"), "activate", G_CALLBACK(onDownloadClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("downloadWholeDirItem"), "activate", G_CALLBACK(onDownloadDirClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("searchByTTHItem"), "activate", G_CALLBACK(onSearchByTTHClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("copyMagnetItem"), "activate", G_CALLBACK(onCopyMagnetClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("getFileListItem"), "activate", G_CALLBACK(onGetFileListClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("openPartial"), "activate", G_CALLBACK(onPartialFileListOpen_gui), (gpointer)this);
    g_signal_connect(getWidget("matchQueueItem"), "activate", G_CALLBACK(onMatchQueueClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("sendPrivateMessageItem"), "activate", G_CALLBACK(onPrivateMessageClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("addToFavoritesItem"), "activate", G_CALLBACK(onAddFavoriteUserClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("grantExtraSlotItem"), "activate", G_CALLBACK(onGrantExtraSlotClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("removeUserFromQueueItem"), "activate", G_CALLBACK(onRemoveUserFromQueueClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("removeItem"), "activate", G_CALLBACK(onRemoveClicked_gui), (gpointer)this);
    g_signal_connect(getWidget("comboboxSize"), "changed", G_CALLBACK(onComboBoxChanged_gui), (gpointer)this);
    g_signal_connect(getWidget("comboboxentrySearch"), "changed", G_CALLBACK(onComboBoxChanged_gui), (gpointer)this);
    g_signal_connect(getWidget("comboboxUnit"), "changed", G_CALLBACK(onComboBoxChanged_gui), (gpointer)this);
    g_signal_connect(getWidget("comboboxFile"), "changed", G_CALLBACK(onComboBoxChanged_gui), (gpointer)this);
    g_signal_connect(getWidget("comboboxGroupBy"), "changed", G_CALLBACK(onGroupByComboBoxChanged_gui), (gpointer)this);
    g_signal_connect(getWidget("togglebuttonSidePanel"), "toggled", G_CALLBACK(onSidePanelToggled_gui), (gpointer)this);
    g_signal_connect(getWidget("buttonClear"), "clicked", G_CALLBACK(onClearButtonClicked_gui), (gpointer)this);
}

Search::~Search()
{
    ClientManager::getInstance()->removeListener(this);
    SearchManager::getInstance()->removeListener(this);
    TimerManager::getInstance()->removeListener(this);

    gtk_widget_destroy(getWidget("dirChooserDialog"));
    g_object_unref(getWidget("mainMenu"));
}

void Search::show()
{
    initHubs_gui();
    ClientManager::getInstance()->addListener(this);
    SearchManager::getInstance()->addListener(this);
    TimerManager::getInstance()->addListener(this);
}

void Search::putValue_gui(const string &str, int64_t size, SearchManager::SizeModes mode, SearchManager::TypeModes type)
{
    gtk_entry_set_text(GTK_ENTRY(getWidget("SearchEntry")), str.c_str());
    gtk_entry_set_text(GTK_ENTRY(getWidget("entrySize")), Util::toString(size).c_str());
    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxSize")), (int)mode);
    gtk_combo_box_set_active(GTK_COMBO_BOX(getWidget("comboboxFile")), (int)type);

    search_gui();
}

void Search::initHubs_gui()
{
    auto lock = ClientManager::getInstance()->lock();

    const Client::List clients = ClientManager::getInstance()->getClients();

    Client *client = nullptr;
    for (auto it = clients.begin(); it != clients.end(); ++it)
    {
        client = *it;
        if (client->isConnected())
            addHub_gui(client->getHubName(), client->getHubUrl());
    }
}

void Search::addHub_gui(string name, string url)
{
    GtkTreeIter iter;
    gtk_list_store_append(hubStore, &iter);
    gtk_list_store_set(hubStore, &iter,
                       hubView.col(_("Search")), true,
                       hubView.col(_("Name")), name.empty() ? url.c_str() : name.c_str(),
                       hubView.col("Url"), url.c_str(),
                       -1);
}

void Search::modifyHub_gui(string name, string url)
{
    GtkTreeIter iter;
    GtkTreeModel *m = GTK_TREE_MODEL(hubStore);
    gboolean valid = gtk_tree_model_get_iter_first(m, &iter);

    while (valid)
    {
        if (url == hubView.getString(&iter, "Url"))
        {
            gtk_list_store_set(hubStore, &iter,
                               hubView.col(_("Name")), name.empty() ? url.c_str() : name.c_str(),
                               hubView.col("Url"), url.c_str(),
                               -1);
            return;
        }
        valid = gtk_tree_model_iter_next(m, &iter);
    }
}

void Search::removeHub_gui(string url)
{
    GtkTreeIter iter;
    GtkTreeModel *m = GTK_TREE_MODEL(hubStore);
    gboolean valid = gtk_tree_model_get_iter_first(m, &iter);

    while (valid)
    {
        if (url == hubView.getString(&iter, "Url"))
        {
            gtk_list_store_remove(hubStore, &iter);
            return;
        }
        valid = gtk_tree_model_iter_next(m, &iter);
    }
}

void Search::popupMenu_gui()
{
    GtkTreeIter iter;
    GtkTreePath *path;
    GList *list = gtk_tree_selection_get_selected_rows(selection, NULL);
    guint count = g_list_length(list);

    if (count == 1)
    {
        path = (GtkTreePath*)list->data; // This will be freed later

        // If it is a parent effectively more than one row is selected
        if (gtk_tree_model_get_iter(sortedFilterModel, &iter, path) &&
                gtk_tree_model_iter_has_child(sortedFilterModel, &iter))
        {
            gtk_widget_set_sensitive(getWidget("searchByTTHItem"), false);
        }
        else
        {
            gtk_widget_set_sensitive(getWidget("searchByTTHItem"), true);
        }
    }
    else if (count > 1)
    {
        gtk_widget_set_sensitive(getWidget("searchByTTHItem"), false);
    }

    GtkWidget *menuItem;
    string tth;
    bool firstTTH;
    bool hasTTH;

    // Clean menus
    gtk_container_foreach(GTK_CONTAINER(getWidget("downloadMenu")), (GtkCallback)gtk_widget_destroy, NULL);
    gtk_container_foreach(GTK_CONTAINER(getWidget("downloadDirMenu")), (GtkCallback)gtk_widget_destroy, NULL);
    userCommandMenu->cleanMenu_gui();

    // Build "Download to..." submenu

    // Add favorite download directories
    StringPairList spl = FavoriteManager::getInstance()->getFavoriteDirs();
    if (!spl.empty())
    {
        for (auto& i : spl)
        {
            menuItem = gtk_menu_item_new_with_label(i.second.c_str());
            g_object_set_data_full(G_OBJECT(menuItem), "fav", g_strdup(i.first.c_str()), g_free);
            g_signal_connect(menuItem, "activate", G_CALLBACK(onDownloadFavoriteClicked_gui), (gpointer)this);
            gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadMenu")), menuItem);
        }
        menuItem = gtk_separator_menu_item_new();
        gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadMenu")), menuItem);
    }

    // Add Browse item
    menuItem = gtk_menu_item_new_with_label(_("Browse..."));
    g_signal_connect(menuItem, "activate", G_CALLBACK(onDownloadToClicked_gui), (gpointer)this);
    gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadMenu")), menuItem);

    // Add search results with the same TTH to menu
    firstTTH = true;
    hasTTH = false;

    for (GList *i = list; i; i = i->next)
    {
        path = (GtkTreePath *)i->data;
        if (gtk_tree_model_get_iter(sortedFilterModel, &iter, path))
        {
            userCommandMenu->addHub(resultView.getString(&iter, "Hub URL"));
            userCommandMenu->addFile(resultView.getString(&iter, "CID"),
                                     resultView.getString(&iter, _("Filename")),
                                     resultView.getString(&iter, _("Path")),
                                     resultView.getValue<int64_t>(&iter, "Real Size"),
                                     resultView.getString(&iter, _("TTH")));

            if (firstTTH)
            {
                tth = resultView.getString(&iter, _("TTH"));
                firstTTH = false;
                hasTTH = true;
            }
            else if (hasTTH)
            {
                if (tth.empty() || tth != resultView.getString(&iter, _("TTH")))
                    hasTTH = false; // Can't break here since we have to free all the paths
            }
        }
        gtk_tree_path_free(path);
    }
    g_list_free(list);

    if (hasTTH)
    {
        StringList targets = QueueManager::getInstance()->getTargets(TTHValue(tth));

        if (!targets.empty())
        {
            menuItem = gtk_separator_menu_item_new();
            gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadMenu")), menuItem);
            for (auto& i : targets)
            {
                menuItem = gtk_menu_item_new_with_label(i.c_str());
                g_signal_connect(menuItem, "activate", G_CALLBACK(onDownloadToMatchClicked_gui), (gpointer)this);
                gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadMenu")), menuItem);
            }
        }
    }

    // Build "Download whole directory to..." submenu

    spl.clear();
    spl = FavoriteManager::getInstance()->getFavoriteDirs();
    if (!spl.empty())
    {
        for (auto& i : spl)
        {
            menuItem = gtk_menu_item_new_with_label(i.second.c_str());
            g_object_set_data_full(G_OBJECT(menuItem), "fav", g_strdup(i.first.c_str()), g_free);
            g_signal_connect(menuItem, "activate", G_CALLBACK(onDownloadFavoriteDirClicked_gui), (gpointer)this);
            gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadDirMenu")), menuItem);
        }
        menuItem = gtk_separator_menu_item_new();
        gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadDirMenu")), menuItem);
    }

    menuItem = gtk_menu_item_new_with_label(_("Browse..."));
    g_signal_connect(menuItem, "activate", G_CALLBACK(onDownloadDirToClicked_gui), (gpointer)this);
    gtk_menu_shell_append(GTK_MENU_SHELL(getWidget("downloadDirMenu")), menuItem);

    // Build user command menu
    userCommandMenu->buildMenu_gui();

#if GTK_CHECK_VERSION(3,22,0)
    gtk_menu_popup_at_pointer(GTK_MENU(getWidget("mainMenu")),NULL);
#else
    gtk_menu_popup(GTK_MENU(getWidget("mainMenu")), NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
#endif
    gtk_widget_show_all(getWidget("mainMenu"));
}

void Search::setStatus_gui(string statusBar, string text)
{
    gtk_statusbar_pop(GTK_STATUSBAR(getWidget(statusBar)), 0);
    gtk_statusbar_push(GTK_STATUSBAR(getWidget(statusBar)), 0,
                       g_filename_to_utf8(text.c_str(),-1,NULL,NULL,NULL));
}

void Search::setProgress_gui(const std::string& progressBar, const std::string& text, float fract)
{
    gtk_progress_bar_set_text(GTK_PROGRESS_BAR(getWidget(progressBar)), text.c_str());
    gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(getWidget(progressBar)), fract);
}

void Search::search_gui()
{
    if (!stop)
    {
        stop = true;
        waitingResults = false;
        gtk_button_set_label(GTK_BUTTON(getWidget("buttonSearch")), _("Search"));
        return;
    }

    StringList clients;
    GtkTreeIter iter;

    string text = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(getWidget("comboboxentrySearch")));
    if (text.empty())
        return;

    gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(hubStore), &iter);
    while (valid)
    {
        if (hubView.getValue<gboolean>(&iter, _("Search")))
            clients.push_back(hubView.getString(&iter, "Url"));
        valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(hubStore), &iter);
    }

#ifndef WITH_DHT
    if (clients.size() < 1)
        return;
#endif

    double lsize = Util::toDouble(gtk_entry_get_text(GTK_ENTRY(getWidget("entrySize"))));

    switch (gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxUnit"))))
    {
    case 1:
        lsize *= 1024.0;
        break;
    case 2:
        lsize *= 1024.0 * 1024.0;
        break;
    case 3:
        lsize *= 1024.0 * 1024.0 * 1024.0;
        break;
    }

    gtk_tree_store_clear(resultStore);
    results.clear();
    int64_t llsize = static_cast<int64_t>(lsize);
    searchlist = StringTokenizer<string>(text, ' ').getTokens();

    // Strip out terms beginning with -
    text.clear();
    for (auto& si : searchlist)
        if (si[0] != '-')
            text += si + ' ';
    text = text.substr(0, std::max(text.size(), static_cast<string::size_type>(1)) - 1);

    SearchManager::SizeModes mode((SearchManager::SizeModes)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxSize"))));
    if (!llsize)
        mode = SearchManager::SIZE_DONTCARE;

    int ftype = gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxFile")));

    string ftypeStr;
    if (ftype > SearchManager::TYPE_ANY && ftype < SearchManager::TYPE_LAST)
    {
        ftypeStr = SearchManager::getInstance()->getTypeStr(ftype);
    }
    else
    {
        gchar *tmp = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(getWidget("comboboxFile")));
        ftypeStr = tmp;
        g_free(tmp);
        ftype = SearchManager::TYPE_ANY;
    }

    // Get ADC searchtype extensions if any is selected
    StringList exts;
    try
    {
        if (ftype == SearchManager::TYPE_ANY)
        {
            // Custom searchtype
            exts = SettingsManager::getInstance()->getExtensions(ftypeStr);
        }
        else if (ftype > SearchManager::TYPE_ANY && ftype < SearchManager::TYPE_DIRECTORY)
        {
            // Predefined searchtype
            exts = SettingsManager::getInstance()->getExtensions(string(1, '0' + ftype));
        }
    }
    catch (const SearchTypeException&)
    {
        ftype = SearchManager::TYPE_ANY;
    }

    isHash = (ftype == SearchManager::TYPE_TTH);

    if (!isHash)
        WSET("last-search-type", (int)ftype);


    // Add new searches to the dropdown list
    GtkListStore *store = GTK_LIST_STORE(searchEntriesModel);
    size_t max = std::max(SETTING(SEARCH_HISTORY) - 1, 0);
    size_t count = 0;
    gchar *entry;
    valid = gtk_tree_model_get_iter_first(searchEntriesModel, &iter);
    while (valid)
    {
        gtk_tree_model_get(searchEntriesModel, &iter, 0, &entry, -1);
        if (text == string(entry) || count >= max)
            valid = gtk_list_store_remove(store, &iter);
        else
            valid = gtk_tree_model_iter_next(searchEntriesModel, &iter);
        count++;
        g_free(entry);
    }

    gtk_list_store_prepend(store, &iter);
    gtk_list_store_set(store, &iter, 0, text.c_str(), -1);

    gtk_widget_show_all(getWidget("statusbox"));
    droppedResult = 0;
    searchHits = 0;
    setStatus_gui("statusbar2", _("Found: 0"));
    setStatus_gui("statusbar3", _("Filtered: 0"));
    setLabel_gui(text);

    searchStartTime = GET_TICK();
    target = text;

    dcdebug(_("Sent ADC extensions : %s\n"), Util::toString(";", exts).c_str());
    uint64_t maxDelayBeforeSearch = SearchManager::getInstance()->search(clients, text, llsize, (SearchManager::TypeModes)ftype, mode, "manual", exts, (void*)this);
    uint64_t waitingResultsTime = 20000; // just assumption that user receives most of results in 20 seconds

    searchEndTime = searchStartTime + maxDelayBeforeSearch + waitingResultsTime;
    waitingResults = true;
    stop = false;

    if (WGETB("clearsearch")) // Only clear if the search was sent.
        gtk_entry_set_text(GTK_ENTRY(searchEntry), "");

    if (gtk_widget_get_visible(getWidget("sidePanel")) &&
            !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(getWidget("checkDontHideSideOnSearch"))))
        gtk_widget_hide(getWidget("sidePanel"));

    gtk_widget_set_sensitive(getWidget("comboboxentrySearch"), false);
    gtk_button_set_label(GTK_BUTTON(getWidget("buttonSearch")), _("Stop"));
}

void Search::addResult_gui(const SearchResultPtr result)
{
    // Check that it's not a duplicate and find parent for grouping
    GtkTreeIter iter;
    GtkTreeIter parent;
    GtkTreeIter child;
    GtkTreeModel *m = GTK_TREE_MODEL(resultStore);
    bool foundParent = false;
    bool createParent = false;

    vector<SearchResultPtr> &existingResults = results[result->getUser()->getCID().toBase32()];
    for (auto& it : existingResults)
    {
        // Check if it's a duplicate
        if (result->getFile() == it->getFile())
            return;
    }
    existingResults.push_back(result);

    dcpp::StringMap resultMap;
    parseSearchResult_gui(result, resultMap);

    // Find grouping parent
    GroupType groupBy = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")));
    string groupColumn = getGroupingColumn(groupBy);
    string groupStr = resultMap[groupColumn];
    gboolean valid = gtk_tree_model_get_iter_first(m, &iter);

    while (valid && groupBy != NOGROUPING && !foundParent && !groupStr.empty())
    {
        // Found a row which matches the grouping criteria
        if (resultView.getString(&iter, groupColumn, m) == groupStr)
        {
            // Parent row
            if (gtk_tree_model_iter_has_child(m, &iter))
            {
                parent = iter;
                foundParent = true;
            }
            // If two rows that match the grouping criteria
            // are found, group them under a new parent row
            else if (!foundParent)
            {
                child = iter;
                createParent = true;
                foundParent = true;
            }
        }

        valid = WulforUtil::getNextIter_gui(m, &iter);
    }

    // Move top level row to be under newly created grouping parent.
    // This needs to be done outside of the loop so that we don't modify the
    // tree until after the duplication check.
    if (createParent)
    {
        parent = createParentRow_gui(child, groupStr);
    }

    // Have to use insert with values since appending would cause searchFilterFunc to be
    // called with empty row which in turn will cause assert failure in treeview::getString
    gtk_tree_store_insert_with_values(resultStore, &iter, foundParent ? &parent : NULL, -1,
                                      resultView.col(_("Nick")), resultMap["Nick"].c_str(),
            resultView.col(_("Filename")), resultMap["Filename"].c_str(),
            resultView.col(_("Slots")), resultMap["Slots"].c_str(),
            resultView.col(_("Size")), resultMap["Size"].c_str(),
            resultView.col(_("Path")), resultMap["Path"].c_str(),
            resultView.col(_("Type")), resultMap["Type"].c_str(),
            resultView.col(_("Connection")), resultMap["Connection"].c_str(),
            resultView.col(_("Hub")), resultMap["Hub"].c_str(),
            resultView.col(_("Exact Size")), resultMap["Exact Size"].c_str(),
            resultView.col(_("IP")), resultMap["IP"].c_str(),
            resultView.col(_("TTH")), resultMap["TTH"].c_str(),
            resultView.col("Icon"), resultMap["Icon"].c_str(),
            resultView.col("File Order"), resultMap["File Order"].c_str(),
            resultView.col("Real Size"), Util::toInt64(resultMap["Real Size"]),
            resultView.col("Slots Order"), Util::toInt(resultMap["Slots Order"]),
            resultView.col("Hub URL"), resultMap["Hub URL"].c_str(),
            resultView.col("CID"), resultMap["CID"].c_str(),
            resultView.col("Shared"), Util::toInt(resultMap["Shared"]),
            resultView.col("Free Slots"), Util::toInt(resultMap["Free Slots"]),
            -1);

    if (foundParent)
        updateParentRow_gui(&parent, &iter);

    ++searchHits;
    setStatus_gui("statusbar2", _("Found: ") + Util::toString(searchHits));

    if (WGETB("bold-search"))
        setBold_gui();
}

/*
  * Move top level row to be under a newly created grouping parent.
  */
GtkTreeIter Search::createParentRow_gui(GtkTreeIter &child, const string &groupStr, const gint position)
{
    GtkTreeIter parent;
    GroupType groupBy = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")));
    string filename = groupStr;

    // As a special case, use the first child's filename for TTH grouping
    if (groupBy == TTH)
        filename = resultView.getString(&child, _("Filename"), GTK_TREE_MODEL(resultStore));

    // Insert the new parent row
    gtk_tree_store_insert_with_values(resultStore, &parent, NULL, position,
                                      resultView.col("Icon"), GTK_STOCK_DND_MULTIPLE,
                                      resultView.col(_("Filename")), filename.c_str(),
                                      -1);

    // Move the row to be a child of the new parent
    GtkTreeIter newChild = WulforUtil::copyRow_gui(resultStore, &child, &parent);
    gtk_tree_store_remove(resultStore, &child);
    child = newChild;

    return parent;
}


void Search::updateParentRow_gui(GtkTreeIter *parent, GtkTreeIter *child)
{
    // Let's make sure we really have children...
    gint children = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(resultStore), parent);
    dcassert(children != 0);

    string users = Util::toString(children) + _(" user(s)");
    gtk_tree_store_set(resultStore, parent, resultView.col(_("Nick")), users.c_str(), -1);

    if (!child)
        return;

    GroupType groupType = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")));

    switch (groupType)
    {
    case NOGROUPING:
        break;
    case FILENAME:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col("File Order"));
        break;
    case FILEPATH:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Path")));
        break;
    case SIZE:
    {
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Exact Size")));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Size")));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col("Real Size"));
        break;
    }
    case CONNECTION:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Connection")));
        break;
    case TTH:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Exact Size")));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Size")));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col("Real Size"));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("TTH")));
        break;
    case NICK:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Nick")));
        break;
    case HUB:
    {
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Hub")));
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col("Hub URL"));
        break;
    }
    case TYPE:
        WulforUtil::copyValue_gui(resultStore, child, parent, resultView.col(_("Type")));
        break;
    default:
        ///@todo: throw an exception
        break;
    }
}

void Search::ungroup_gui()
{
    GtkTreeIter iter;
    gint position = 0;
    GtkTreeModel *m = GTK_TREE_MODEL(resultStore);
    gboolean valid = gtk_tree_model_get_iter_first(m, &iter);

    while (valid)
    {
        // Ungroup parent rows and remove them
        if (gtk_tree_model_iter_has_child(m, &iter))
        {
            GtkTreeIter child = iter;
            valid = WulforUtil::getNextIter_gui(m, &child, true, false);

            // Move all children out from under the old grouping parent
            while (valid)
            {
                WulforUtil::copyRow_gui(resultStore, &child, NULL, position++);
                valid = gtk_tree_store_remove(resultStore, &child);
            }

            // Delete the parent row
            valid = gtk_tree_store_remove(resultStore, &iter);
        }
        else // Non-parent row
        {
            ++position;
            valid = WulforUtil::getNextIter_gui(m, &iter);
        }
    }
}

void Search::regroup_gui()
{
    unordered_map<string, GtkTreeIter> iterMap; // Maps group string -> parent tree iter
    GtkTreeIter iter;
    gint position = 0;
    GtkTreeModel *m = GTK_TREE_MODEL(resultStore);
    GroupType groupBy = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")));
    string groupColumn = getGroupingColumn(groupBy);
    gboolean valid = gtk_tree_model_get_iter_first(m, &iter);

    while (valid)
    {
        string groupStr;
        if (!groupColumn.empty())
            groupStr = resultView.getString(&iter, groupColumn, m);

        // Don't add parent rows
        if (gtk_tree_model_iter_has_child(m, &iter) || groupStr.empty())
        {
            ++position;
            valid = WulforUtil::getNextIter_gui(m, &iter);
            continue;
        }

        auto mapIter = iterMap.find(groupStr);

        // New non-parent, top-level item
        if (mapIter == iterMap.end())
        {
            ++position;
            iterMap[groupStr] = iter;
            valid = WulforUtil::getNextIter_gui(m, &iter);
        }
        else // Insert as a child under the grouping parent
        {
            GtkTreeIter parent = mapIter->second;

            // If this is the first child to be appended, create a new parent row.
            if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(resultStore), &parent))
            {
                GtkTreeIter child = parent;
                parent = createParentRow_gui(child, groupStr, position);
                updateParentRow_gui(&parent, &child);

                mapIter->second = parent;
            }

            // Insert the row as a child
            WulforUtil::copyRow_gui(resultStore, &iter, &parent);
            valid = gtk_tree_store_remove(resultStore, &iter);
            updateParentRow_gui(&parent);
        }
    }
}

/*
 * We can't rely on the string from the text box since it will be internationalized.
 */
string Search::getGroupingColumn(GroupType groupBy)
{
    string column;

    switch (groupBy)
    {
    case Search::NOGROUPING:
        break;
    case Search::FILENAME:
        column = _("Filename");
        break;
    case Search::FILEPATH:
        column = _("Path");
        break;
    case Search::SIZE:
        column = _("Size");
        break;
    case Search::CONNECTION:
        column = _("Connection");
        break;
    case Search::TTH:
        column = _("TTH");
        break;
    case Search::NICK:
        column = _("Nick");
        break;
    case Search::HUB:
        column = _("Hub");
        break;
    case Search::TYPE:
        column = _("Type");
        break;
    default:
        ///@todo: throw an exception
        break;
    }

    return column;
}

void Search::download_gui(const string &target)
{
    if (target.empty() || gtk_tree_selection_count_selected_rows(selection) <= 0)
        return;

    GtkTreeIter iter;
    GtkTreePath *path;
    GroupType groupBy = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(getWidget("comboboxGroupBy")));
    GList *list = gtk_tree_selection_get_selected_rows(selection, NULL);
    typedef Func6<Search, string, string, string, int64_t, string, string> F6;

    for (GList *i = list; i; i = i->next)
    {
        path = (GtkTreePath *)i->data;
        if (gtk_tree_model_get_iter(sortedFilterModel, &iter, path))
        {
            bool parent = gtk_tree_model_iter_has_child(sortedFilterModel, &iter);
            string filename = resultView.getString(&iter, _("Filename"));

            do
            {
                if (!gtk_tree_model_iter_has_child(sortedFilterModel, &iter))
                {
                    // User parent filename when grouping by TTH to avoid downloading the same file multiple times
                    if (groupBy != TTH || resultView.getString(&iter, _("Type")) == _("Directory"))
                    {
                        filename = resultView.getString(&iter, _("Path"));
                        filename += resultView.getString(&iter, _("Filename"));
                    }

                    string cid = resultView.getString(&iter, "CID");
                    int64_t size = resultView.getValue<int64_t>(&iter, "Real Size");
                    string tth = resultView.getString(&iter, _("TTH"));
                    string hubUrl = resultView.getString(&iter, "Hub URL");
                    F6 *func = new F6(this, &Search::download_client, target, cid, filename, size, tth, hubUrl);
                    WulforManager::get()->dispatchClientFunc(func);
                }
            }
            while (parent && WulforUtil::getNextIter_gui(sortedFilterModel, &iter, true, false));
        }
        gtk_tree_path_free(path);
    }
    g_list_free(list);
}

gboolean Search::onFocusIn_gui(GtkWidget*, GdkEventFocus*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;

    gtk_widget_grab_focus(s->getWidget("comboboxentrySearch"));

    return true;
}

gboolean Search::onButtonPressed_gui(GtkWidget*, GdkEventButton *event, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;
    s->oldEventType = event->type;

    if (event->button == 3)
    {
        GtkTreePath *path;
        if (gtk_tree_view_get_path_at_pos(s->resultView.get(), (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
        {
            bool selected = gtk_tree_selection_path_is_selected(s->selection, path);
            gtk_tree_path_free(path);

            if (selected)
                return true;
        }
    }
    return false;
}

gboolean Search::onButtonReleased_gui(GtkWidget*, GdkEventButton *event, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;
    gint count = gtk_tree_selection_count_selected_rows(s->selection);

    if (count > 0 && event->type == GDK_BUTTON_RELEASE && event->button == 3)
        s->popupMenu_gui();
    else if (count == 1 && s->oldEventType == GDK_2BUTTON_PRESS && event->button == 1)
        s->onDownloadClicked_gui(NULL, data);

    return false;
}

gboolean Search::onKeyReleased_gui(GtkWidget *widget, GdkEventKey *event, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;

    if (widget == GTK_WIDGET(s->resultView.get()))
    {
        gint count = gtk_tree_selection_count_selected_rows(s->selection);

        if (count > 0)
        {
            if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter)
                s->onDownloadClicked_gui(NULL, data);
            else if (event->keyval == GDK_KEY_Delete || event->keyval == GDK_KEY_BackSpace)
                s->onRemoveClicked_gui(NULL, data);
            else if (event->keyval == GDK_KEY_Menu || (event->keyval == GDK_KEY_F10 && event->state & GDK_SHIFT_MASK))
                s->popupMenu_gui();
        }
    }
    else
    {
        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
            gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
    }

    return false;
}

gboolean Search::onSearchEntryKeyPressed_gui(GtkWidget*, GdkEventKey *event, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;

    if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter)
    {
        s->search_gui();
    }
    else if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down)
    {
        gtk_combo_box_popup(GTK_COMBO_BOX(s->getWidget("comboboxentrySearch")));
        return true;
    }

    return false;
}

void Search::onComboBoxChanged_gui(GtkWidget*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
        gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
}

void Search::onGroupByComboBoxChanged_gui(GtkWidget *comboBox, gpointer data)
{
    Search *s = (Search*)data;
    GroupType groupBy = (GroupType)gtk_combo_box_get_active(GTK_COMBO_BOX(comboBox));

    s->ungroup_gui();

    if (groupBy != NOGROUPING)
    {
        gtk_widget_set_sensitive(s->getWidget("checkbuttonFilter"), false);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonSlots"), false);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonShared"), false);
        s->regroup_gui();
    }
    else
    {
        gtk_widget_set_sensitive(s->getWidget("checkbuttonFilter"), true);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonSlots"), false);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonShared"), false);
    }
}

void Search::onSearchButtonClicked_gui(GtkWidget*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;
    s->search_gui();
}

void Search::onFilterButtonToggled_gui(GtkToggleButton *button, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;
    GtkComboBox *comboBox = GTK_COMBO_BOX(s->getWidget("comboboxGroupBy"));

    // Disable grouping when filtering within local results
    if (gtk_toggle_button_get_active(button))
    {
        s->previousGrouping = (GroupType)gtk_combo_box_get_active(comboBox);
        gtk_combo_box_set_active(comboBox, (int)NOGROUPING);
        gtk_widget_set_sensitive(GTK_WIDGET(comboBox), false);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonSlots"), true);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonShared"), true);
    }
    else
    {
        gtk_combo_box_set_active(comboBox, (int)s->previousGrouping);
        gtk_widget_set_sensitive(GTK_WIDGET(comboBox), true);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonSlots"), false);
        gtk_widget_set_sensitive(s->getWidget("checkbuttonShared"), false);

    }

    gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
}

void Search::onSlotsButtonToggled_gui(GtkToggleButton *button, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    s->onlyFree = gtk_toggle_button_get_active(button);
    if (s->onlyFree != BOOLSETTING(SEARCH_ONLY_FREE_SLOTS))
        SettingsManager::getInstance()->set(SettingsManager::SEARCH_ONLY_FREE_SLOTS, s->onlyFree);

    // Refilter current view only if "Search within local results" is enabled
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
        gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
}

void Search::onSharedButtonToggled_gui(GtkToggleButton *button, gpointer data)
{
    (void)button;
    Search *s = (Search *)data;
    if (!s) return;

    // Refilter current view only if "Search within local results" is enabled
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
        gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
}

void Search::onToggledClicked_gui(GtkCellRendererToggle *cell, gchar *path, gpointer data)
{
    (void)cell;
    Search *s = (Search *)data;
    if (!s) return;
    GtkTreeIter iter;

    if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(s->hubStore), &iter, path))
    {
        gboolean toggled = s->hubView.getValue<gboolean>(&iter, _("Search"));
        gtk_list_store_set(s->hubStore, &iter, s->hubView.col(_("Search")), !toggled, -1);
    }

    // Refilter current view only if "Search within local results" is enabled
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
        gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(s->searchFilterModel));
}

void Search::onDownloadClicked_gui(GtkMenuItem *item, gpointer data)
{
    (void)item;
    Search *s = (Search *)data;
    if (!s) return;

    string target = SETTING(DOWNLOAD_DIRECTORY);
    s->download_gui(target);
}

void Search::onDownloadFavoriteClicked_gui(GtkMenuItem *item, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;
    string fav = string((gchar *)g_object_get_data(G_OBJECT(item), "fav"));

    s->download_gui(fav);
}

void Search::onDownloadToClicked_gui(GtkMenuItem *item, gpointer data)
{
    (void)item;
    Search *s = (Search *)data;
    if (!s) return;

    gint response = gtk_dialog_run(GTK_DIALOG(s->getWidget("dirChooserDialog")));

    // Fix crash, if the dialog gets programmatically destroyed.
    if (response == GTK_RESPONSE_NONE)
        return;

    gtk_widget_hide(s->getWidget("dirChooserDialog"));

    if (response == GTK_RESPONSE_OK)
    {
        gchar *temp = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(s->getWidget("dirChooserDialog")));

        if (temp)
        {
            string target = Text::toUtf8(temp);
            g_free(temp);

            if (target[target.length() - 1] != PATH_SEPARATOR)
                target += PATH_SEPARATOR;

            s->download_gui(target);
        }
    }
}

void Search::onDownloadToMatchClicked_gui(GtkMenuItem *item, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        string fileName = WulforUtil::getTextFromMenu(item);
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func5<Search, string, string, int64_t, string, string> F5;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    if (!gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter))
                    {
                        string cid = s->resultView.getString(&iter, "CID");
                        int64_t size = s->resultView.getValue<int64_t>(&iter, "Real Size");
                        string tth = s->resultView.getString(&iter, _("TTH"));
                        string hubUrl = s->resultView.getString(&iter, "Hub URL");
                        F5 *func = new F5(s, &Search::addSource_client, fileName, cid, size, tth, hubUrl);
                        WulforManager::get()->dispatchClientFunc(func);
                    }
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onDownloadDirClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        string target = SETTING(DOWNLOAD_DIRECTORY);
        typedef Func4<Search, string, string, string, string> F4;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    if (!gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter))
                    {
                        string cid = s->resultView.getString(&iter, "CID");
                        string filename = s->resultView.getString(&iter, _("Path"));
                        filename += s->resultView.getString(&iter, _("Filename"));
                        string hubUrl = s->resultView.getString(&iter, "Hub URL");
                        F4 *func = new F4(s, &Search::downloadDir_client, target, cid, filename, hubUrl);
                        WulforManager::get()->dispatchClientFunc(func);
                    }
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onDownloadFavoriteDirClicked_gui(GtkMenuItem *item, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;
    string fav = (gchar *)g_object_get_data(G_OBJECT(item), "fav");

    if (!fav.empty() && gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func4<Search, string, string, string, string> F4;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    if (!gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter))
                    {
                        string cid = s->resultView.getString(&iter, "CID");
                        string filename = s->resultView.getString(&iter, _("Path"));
                        filename += s->resultView.getString(&iter, _("Filename"));
                        string hubUrl = s->resultView.getString(&iter, "Hub URL");
                        F4 *func = new F4(s, &Search::downloadDir_client, fav, cid, filename, hubUrl);
                        WulforManager::get()->dispatchClientFunc(func);
                    }
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onDownloadDirToClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    gint response = gtk_dialog_run(GTK_DIALOG(s->getWidget("dirChooserDialog")));

    // Fix crash, if the dialog gets programmatically destroyed.
    if (response == GTK_RESPONSE_NONE)
        return;

    gtk_widget_hide(s->getWidget("dirChooserDialog"));

    if (response == GTK_RESPONSE_OK)
    {
        int count = gtk_tree_selection_count_selected_rows(s->selection);
        gchar *temp = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(s->getWidget("dirChooserDialog")));

        if (temp && count > 0)
        {
            string target = Text::toUtf8(temp);
            g_free(temp);

            if (target[target.length() - 1] != PATH_SEPARATOR)
                target += PATH_SEPARATOR;

            GtkTreeIter iter;
            GtkTreePath *path;
            GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
            typedef Func4<Search, string, string, string, string> F4;

            for (GList *i = list; i; i = i->next)
            {
                path = (GtkTreePath *)i->data;
                if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
                {
                    bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                    do
                    {
                        if (!gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter))
                        {
                            string cid = s->resultView.getString(&iter, "CID");
                            string filename = s->resultView.getString(&iter, _("Path"));
                            filename += s->resultView.getString(&iter, _("Filename"));
                            string hubUrl = s->resultView.getString(&iter, "Hub URL");
                            F4 *func = new F4(s, &Search::downloadDir_client, target, cid, filename, hubUrl);
                            WulforManager::get()->dispatchClientFunc(func);
                        }
                    }
                    while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
                }
                gtk_tree_path_free(path);
            }
            g_list_free(list);
        }
    }
}

void Search::onSearchByTTHClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                string tth = s->resultView.getString(&iter, _("TTH"));
                if (!tth.empty())
                {
                    s = WulforManager::get()->getMainWindow()->addSearch_gui();
                    s->putValue_gui(tth, 0, SearchManager::SIZE_DONTCARE, SearchManager::TYPE_TTH);
                }
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onGetFileListClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func5<Search, string, string, bool, string, bool> F5;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    string cid = s->resultView.getString(&iter, "CID");
                    string dir = s->resultView.getString(&iter, _("Path"));
                    string hubUrl = s->resultView.getString(&iter, "Hub URL");
                    F5 *func = new F5(s, &Search::getFileList_client, cid, dir, false, hubUrl, true);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onPartialFileListOpen_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func5<Search, string, string, bool, string, bool> F5;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    string cid = s->resultView.getString(&iter, "CID");
                    string dir = s->resultView.getString(&iter, _("Path"));
                    string hubUrl = s->resultView.getString(&iter, "Hub URL");
                    F5 *func = new F5(s, &Search::getFileList_client, cid, dir, false, hubUrl, false);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onMatchQueueClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func5<Search, string, string, bool, string, bool> F5;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    string cid = s->resultView.getString(&iter, "CID");
                    string hubUrl = s->resultView.getString(&iter, "Hub URL");
                    F5 *func = new F5(s, &Search::getFileList_client, cid, "", true, hubUrl, true);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onPrivateMessageClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    string cid = s->resultView.getString(&iter, "CID");
                    string hubUrl = s->resultView.getString(&iter, "Hub URL");
                    if (!cid.empty())
                        WulforManager::get()->getMainWindow()->addPrivateMessage_gui(Msg::UNKNOWN, cid, hubUrl);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onAddFavoriteUserClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        string cid;
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func1<Search, string> F1;
        F1 *func;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    cid = s->resultView.getString(&iter, "CID");
                    func = new F1(s, &Search::addFavUser_client, cid);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onGrantExtraSlotClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func2<Search, string, string> F2;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    string cid = s->resultView.getString(&iter, "CID");
                    string hubUrl = s->resultView.getString(&iter, "Hub URL");
                    F2 *func = new F2(s, &Search::grantSlot_client, cid, hubUrl);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

void Search::onRemoveUserFromQueueClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        string cid;
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);
        typedef Func1<Search, string> F1;
        F1 *func;

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    cid = s->resultView.getString(&iter, "CID");
                    func = new F1(s, &Search::removeSource_client, cid);
                    WulforManager::get()->dispatchClientFunc(func);
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);
    }
}

// Removing a row from treeStore still leaves the SearchResultPtr to results map. This way if a duplicate
// result comes in later it won't be readded, before the results map is cleared with a new search.
void Search::onRemoveClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        GtkTreeIter iter;
        GtkTreeIter filterIter;
        GtkTreePath *path;
        vector<GtkTreeIter> remove;
        GList *list = g_list_reverse(gtk_tree_selection_get_selected_rows(s->selection, NULL));
        GList *refList = NULL;

        // Convert it to list of GtkTreeRowReferences since modifying the model with a list of Paths is bad.
        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            refList = g_list_append(refList, gtk_tree_row_reference_new(s->sortedFilterModel, path));
            gtk_tree_path_free(path);
        }
        g_list_free(list);
        for (GList *i = refList; i; i = i->next)
        {
            path = gtk_tree_row_reference_get_path((GtkTreeRowReference*)i->data);
            if (path)
            {
                if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
                {
                    // Remove the top-level node and it will remove any children nodes (if applicable)
                    gtk_tree_model_sort_convert_iter_to_child_iter(GTK_TREE_MODEL_SORT(s->sortedFilterModel), &filterIter, &iter);
                    gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(s->searchFilterModel), &iter, &filterIter);
                    gtk_tree_store_remove(s->resultStore, &iter);
                }
                gtk_tree_path_free(path);
            }
            gtk_tree_row_reference_free((GtkTreeRowReference*)i->data);
        }
        g_list_free(refList);
    }
}

void Search::onCopyMagnetClicked_gui(GtkMenuItem*, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return;

    if (gtk_tree_selection_count_selected_rows(s->selection) > 0)
    {
        int64_t size;
        string magnets, magnet, filename, tth;
        GtkTreeIter iter;
        GtkTreePath *path;
        GList *list = gtk_tree_selection_get_selected_rows(s->selection, NULL);

        for (GList *i = list; i; i = i->next)
        {
            path = (GtkTreePath *)i->data;
            if (gtk_tree_model_get_iter(s->sortedFilterModel, &iter, path))
            {
                bool parent = gtk_tree_model_iter_has_child(s->sortedFilterModel, &iter);

                do
                {
                    filename = s->resultView.getString(&iter, _("Filename"));
                    size = s->resultView.getValue<int64_t>(&iter, "Real Size");
                    tth = s->resultView.getString(&iter, _("TTH"));
                    magnet = WulforUtil::makeMagnet(filename, size, tth);

                    if (!magnet.empty())
                    {
                        if (!magnets.empty())
                            magnets += '\n';
                        magnets += magnet;
                    }
                }
                while (parent && WulforUtil::getNextIter_gui(s->sortedFilterModel, &iter, true, false));
            }
            gtk_tree_path_free(path);
        }
        g_list_free(list);

        if (!magnets.empty())
            gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), magnets.c_str(), magnets.length());
    }
}

void Search::parseSearchResult_gui(SearchResultPtr result, StringMap &resultMap)
{
    if (result->getType() == SearchResult::TYPE_FILE)
    {
        string file = WulforUtil::linuxSeparator(result->getFile());
        if (file.rfind('/') == tstring::npos)
        {
            resultMap["Filename"] = file;
        }
        else
        {
            resultMap["Filename"] = Util::getFileName(file);
            resultMap["Path"] = Util::getFilePath(file);
        }

        resultMap["File Order"] = "f" + resultMap["Filename"];
        resultMap["Type"] = Util::getFileExt(resultMap["Filename"]);
        if (!resultMap["Type"].empty() && resultMap["Type"][0] == '.')
            resultMap["Type"].erase(0, 1);
        resultMap["Size"] = Util::formatBytes(result->getSize());
        resultMap["Exact Size"] = Util::formatExactSize(result->getSize());
        resultMap["Icon"] = "icon-file";
        resultMap["Shared"] = Util::toString(ShareManager::getInstance()->isTTHShared(result->getTTH()));
    }
    else
    {
        string path = WulforUtil::linuxSeparator(result->getFile());
        resultMap["Filename"] = Util::getLastDir(path) + PATH_SEPARATOR;
        resultMap["Path"] = Util::getFilePath(path.substr(0, path.length() - 1)); // getFilePath just returns path unless we chop the last / off
        if (resultMap["Path"].find("/") == string::npos)
            resultMap["Path"] = "";
        resultMap["File Order"] = "d" + resultMap["Filename"];
        resultMap["Type"] = _("Directory");
        resultMap["Icon"] = "icon-directory";
        resultMap["Shared"] = "0";
        if (result->getSize() > 0)
        {
            resultMap["Size"] = Util::formatBytes(result->getSize());
            resultMap["Exact Size"] = Util::formatExactSize(result->getSize());
        }
    }

    resultMap["Nick"] = WulforUtil::getNicks(result->getUser(), result->getHubURL());
    resultMap["CID"] = result->getUser()->getCID().toBase32();
    resultMap["Slots"] = result->getSlotString();
    resultMap["Connection"] = ClientManager::getInstance()->getConnection(result->getUser()->getCID());
    resultMap["Hub"] = result->getHubName().empty() ? result->getHubURL().c_str() : result->getHubName().c_str();
    resultMap["Hub URL"] = result->getHubURL();
    resultMap["IP"] = result->getIP();
    resultMap["Real Size"] = Util::toString(result->getSize());
    if (result->getType() == SearchResult::TYPE_FILE)
        resultMap["TTH"] = result->getTTH().toBase32();

    // assumption: total slots is never above 999
    resultMap["Slots Order"] = Util::toString(-1000 * result->getFreeSlots() - result->getSlots());
    resultMap["Free Slots"] = Util::toString(result->getFreeSlots());
}

void Search::download_client(string target, string cid, string filename, int64_t size, string tth, string hubUrl)
{
    try
    {
        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (!user)
            return;

        // Only files have a TTH
        if (!tth.empty())
        {
            string subdir = Util::getFileName(filename);
            QueueManager::getInstance()->add(target + subdir, size, TTHValue(tth), HintedUser(user, hubUrl));
        }
        else
        {
            string dir = WulforUtil::windowsSeparator(filename);
            QueueManager::getInstance()->addDirectory(dir, HintedUser(user, hubUrl), target);
        }
    }
    catch (const Exception&)
    {
    }
}

void Search::downloadDir_client(string target, string cid, string filename, string hubUrl)
{
    try
    {
        string dir;

        // If it's a file (directories are assumed to end in '/')
        if (filename[filename.length() - 1] != PATH_SEPARATOR)
        {
            dir = WulforUtil::windowsSeparator(Util::getFilePath(filename));
        }
        else
        {
            dir = WulforUtil::windowsSeparator(filename);
        }

        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (user)
        {
            QueueManager::getInstance()->addDirectory(dir, HintedUser(user, hubUrl), target);
        }
    }
    catch (const Exception&)
    {
    }
}

void Search::addSource_client(string source, string cid, int64_t size, string tth, string hubUrl)
{
    try
    {
        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (!tth.empty() && user)
        {
            QueueManager::getInstance()->add(source, size, TTHValue(tth), HintedUser(user, hubUrl));
        }
    }
    catch (const Exception&)
    {
    }
}

void Search::getFileList_client(string cid, string dir, bool match, string hubUrl, bool full)
{
    if (!cid.empty())
    {
        try
        {
            UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
            if (user)
            {
                int flags = 0;

                if (match)
                    flags = QueueItem::FLAG_MATCH_QUEUE;
                else
                    flags = QueueItem::FLAG_CLIENT_VIEW;

                if (!full)
                    flags = QueueItem::FLAG_CLIENT_VIEW | QueueItem::FLAG_PARTIAL_LIST;

                QueueManager::getInstance()->addList(HintedUser(user, hubUrl), flags, dir);
            }
        }
        catch (const Exception&)
        {
        }
    }
}

void Search::grantSlot_client(string cid, string hubUrl)
{
    if (!cid.empty())
    {
        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (user)
        {
            UploadManager::getInstance()->reserveSlot(HintedUser(user, hubUrl));
        }
    }
}

void Search::addFavUser_client(string cid)
{
    if (!cid.empty())
    {
        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (user)
            FavoriteManager::getInstance()->addFavoriteUser(user);
    }
}

void Search::removeSource_client(string cid)
{
    if (!cid.empty())
    {
        UserPtr user = ClientManager::getInstance()->findUser(CID(cid));
        if (user)
            QueueManager::getInstance()->removeSource(user, QueueItem::Source::FLAG_REMOVED);
    }
}

void Search::on(ClientManagerListener::ClientConnected, Client *client) noexcept
{
    if (client)
    {
        typedef Func2<Search, string, string> F2;
        F2 *func = new F2(this, &Search::addHub_gui, client->getHubName(), client->getHubUrl());
        WulforManager::get()->dispatchGuiFunc(func);
    }
}

void Search::on(ClientManagerListener::ClientUpdated, Client *client) noexcept
{
    if (client)
    {
        typedef Func2<Search, string, string> F2;
        F2 *func = new F2(this, &Search::modifyHub_gui, client->getHubName(), client->getHubUrl());
        WulforManager::get()->dispatchGuiFunc(func);
    }
}

void Search::on(ClientManagerListener::ClientDisconnected, Client *client) noexcept
{
    if (client)
    {
        typedef Func1<Search, string> F1;
        F1 *func = new F1(this, &Search::removeHub_gui, client->getHubUrl());
        WulforManager::get()->dispatchGuiFunc(func);
    }
}

void Search::on(TimerManagerListener::Second, uint64_t aTick) noexcept {
    struct UpdateProgressArgs { Search *search; uint64_t aTick; };

    auto updateProgress = [](gpointer data) noexcept -> gboolean {
        auto args = static_cast<UpdateProgressArgs *>(data);
        auto search = args->search;
        if (search->waitingResults)
        {
            // use rounding below to workaround bug in gtk_progress_bar_set_fraction()
            uint fract  = (1000 * (args->aTick - search->searchStartTime)) / (search->searchEndTime - search->searchStartTime);
            float fraction  = 1.0f * fract / 1000;
            if (fraction >= 1.0)
            {
                fraction = 1.0;
                search->waitingResults = false;
            }
            search->setProgress_gui("progressbar1", _("Searching for ") + search->target + string(" ..."), fraction);
        }
        else
        {
            search->setProgress_gui("progressbar1", "", 0.0);
            gtk_widget_set_sensitive(search->getWidget("comboboxentrySearch"), true);
        }
        return G_SOURCE_REMOVE;
    };

    auto args = g_new0(UpdateProgressArgs, 1);
    args->search = this;
    args->aTick = aTick;
    // Run updateProgress on the main thread
    g_idle_add_full(G_PRIORITY_DEFAULT, updateProgress, args, g_free);
}

void Search::on(SearchManagerListener::SR, const SearchResultPtr& result) noexcept
{
    if (searchlist.empty() || !result || stop)
        return;

    typedef Func2<Search, string, string> F2;

    if (isHash)
    {
        if (result->getType() != SearchResult::TYPE_FILE || TTHValue(searchlist[0]) != result->getTTH())
        {
            ++droppedResult;
            F2 *func = new F2(this, &Search::setStatus_gui, "statusbar3", _("Filtered: ") + Util::toString(droppedResult));
            WulforManager::get()->dispatchGuiFunc(func);
            return;
        }
    }
    else
    {
        for (TStringIter i = searchlist.begin(); i != searchlist.end(); ++i)
        {
            if ((*i->begin() != '-' && Util::findSubString(result->getFile(), *i) == (string::size_type)-1) ||
                    (*i->begin() == '-' && i->size() != 1 && Util::findSubString(result->getFile(), i->substr(1)) != (string::size_type)-1))
            {
                ++droppedResult;
                F2 *func = new F2(this, &Search::setStatus_gui, "statusbar3", _("Dropped: ") + Util::toString(droppedResult));
                WulforManager::get()->dispatchGuiFunc(func);
                return;
            }
        }
    }

    typedef Func1<Search, SearchResultPtr> F1;
    F1 *func = new F1(this, &Search::addResult_gui, result);
    WulforManager::get()->dispatchGuiFunc(func);
}

// Filtering causes Gtk-CRITICAL assertion failure, when last item is removed
// see. https://bugzilla.gnome.org/show_bug.cgi?id=464173
gboolean Search::searchFilterFunc_gui(GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
{
    Search *s = (Search *)data;
    if (!s) return false;
    dcassert(model == GTK_TREE_MODEL(s->resultStore));

    // Enabler filtering only if search within local results is checked
    if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonFilter"))))
        return true;

    // Grouping shouldn't be enabled while filtering, but just in case...
    if (gtk_tree_model_iter_has_child(model, iter))
        return true;

    string hub = s->resultView.getString(iter, "Hub URL", model);
    GtkTreeIter hubIter;
    bool valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(s->hubStore), &hubIter);
    while (valid)
    {
        if (hub == s->hubView.getString(&hubIter, "Url"))
        {
            if (!s->hubView.getValue<gboolean>(&hubIter, _("Search")))
                return false;
            else
                break;
        }
        valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(s->hubStore), &hubIter);
    }

    // Filter based on free slots.
    gint freeSlots = s->resultView.getValue<gint>(iter, "Free Slots", model);
    if (s->onlyFree && freeSlots < 1)
        return false;

    // Hide results already in share
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->getWidget("checkbuttonShared"))) &&
            s->resultView.getValue<gboolean>(iter, "Shared", model) == true)
        return false;

    // Filter based on search terms.
    string filter = Text::toLower(gtk_entry_get_text(GTK_ENTRY(s->searchEntry)));
    TStringList filterList = StringTokenizer<tstring>(filter, ' ').getTokens();
    string filename = Text::toLower(s->resultView.getString(iter, _("Filename"), model));
    string path = Text::toLower(s->resultView.getString(iter, _("Path"), model));
    for (auto& term : filterList)
    {
        if (term[0] == '-')
        {
            if (filename.find(term.substr(1)) != string::npos)
                return false;
            else if (path.find(term.substr(1)) != string::npos)
                return false;
        }
        else if (filename.find(term) == string::npos && path.find(term) == string::npos)
            return false;
    }

    // Filter based on file size.
    double filterSize = Util::toDouble(gtk_entry_get_text(GTK_ENTRY(s->getWidget("entrySize"))));
    if (filterSize > 0)
    {
        switch (gtk_combo_box_get_active(GTK_COMBO_BOX(s->getWidget("comboboxUnit"))))
        {
        case 1:
            filterSize *= 1024.0;
            break;
        case 2:
            filterSize *= 1024.0 * 1024.0;
            break;
        case 3:
            filterSize *= 1024.0 * 1024.0 * 1024.0;
            break;
        }

        int64_t size = s->resultView.getValue<int64_t>(iter, "Real Size", model);

        switch (gtk_combo_box_get_active(GTK_COMBO_BOX(s->getWidget("comboboxSize"))))
        {
        case 0:
            if (size != filterSize)
                return false;
            break;
        case 1:
            if (size < filterSize)
                return false;
            break;
        case 2:
            if (size > filterSize)
                return false;
        }
    }

    int type = gtk_combo_box_get_active(GTK_COMBO_BOX(s->getWidget("comboboxFile")));
    if (type != SearchManager::TYPE_ANY && type != ShareManager::getInstance()->getType(filename))
        return false;

    return true;
}

void Search::onSidePanelToggled_gui(GtkWidget *widget, gpointer data)
{
    (void)widget;
    Search *s = (Search *)data;
    if (!s) return;
    GtkWidget *sidepanel = (GtkWidget *)s->getWidget("sidePanel");

    if (gtk_widget_get_visible(sidepanel))
        gtk_widget_hide(sidepanel);
    else
        gtk_widget_show_all(sidepanel);
}

void Search::onClearButtonClicked_gui(GtkWidget *widget, gpointer data)
{
    (void)widget;
    Search *s = (Search *)data;
    if (!s) return;
    gtk_tree_store_clear(s->resultStore);
    s->results.clear();
    gtk_entry_set_text(GTK_ENTRY(s->searchEntry), "");
    gtk_entry_set_text(GTK_ENTRY(s->getWidget("entrySize")), "");
}
