<style>
    #searchPattern {
        width: 300px;
        background-image: url("images/edit-find.svg");
        background-repeat: no-repeat;
        background-size: 1.5em;
        background-position: left;
        border: 1px solid var(--color-border-default);
        border-radius: 3px;
        padding: 4px 2px 4px 25px;
    }

    #categorySelect {
        width: 150px;
    }

    #pluginsSelect {
        width: 150px;
    }

    #searchResultsNoPlugins,
    #searchResultsNoSearches {
        height: calc(100% - 110px);

        table {
            height: 100%;
            width: 100%;
            text-align: center;
        }
    }

    #searchResultsFilters {
        height: 30px;
        overflow: auto;
    }

    #searchInNameFilter {
        width: 150px;
        margin-right: 20px;
    }

    #searchMinSeedsFilter,
    #searchMaxSeedsFilter,
    #searchMinSizeFilter,
    #searchMaxSizeFilter {
        width: 4em;
    }

    #manageSearchPlugins {
        float: right;
    }

    #searchResultsGranularFilters {
        display: none;
    }

    #searchResultsGranularFiltersWarning {
        vertical-align: bottom;
        margin-left: 10px;
    }

    @media (min-width: 1060px) {
        #searchResultsGranularFilters {
            display: inline-block;
        }

        #searchResultsGranularFiltersWarning {
            display: none;
        }

        #searchPattern {
            width: 500px;
        }
    }

</style>

<div id="searchResults">
    <div style="overflow: hidden; height: 60px;">
        <div style="margin: 20px 0 10px 0; height: 30px;">
            <input type="text" id="searchPattern" class="searchInputField" placeholder="QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocomplete="off" autocapitalize="none" oninput="qBittorrent.Search.onSearchPatternChanged()" />
            <select id="categorySelect" class="searchInputField" onchange="qBittorrent.Search.categorySelected()"></select>
            <select id="pluginsSelect" class="searchInputField" onchange="qBittorrent.Search.pluginSelected()"></select>
            <button type="button" id="startSearchButton" class="searchInputField" onclick="qBittorrent.Search.startStopSearch()">QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]</button>
        </div>
    </div>

    <div id="searchResultsNoPlugins">
        <table>
            <tbody>
                <tr>
                    <td>
                        QBT_TR(There aren't any search plugins installed.)QBT_TR[CONTEXT=SearchEngineWidget]
                        <br />
                        QBT_TR(Click the "Search plugins..." button at the bottom right of the window to install some.)QBT_TR[CONTEXT=SearchEngineWidget]
                    </td>
                </tr>
            </tbody>
        </table>
        <span></span>
    </div>

    <div id="searchResultsNoSearches" style="display: none">
        <table>
            <tbody>
                <tr>
                    <td>
                        QBT_TR(Start a search above.)QBT_TR[CONTEXT=SearchEngineWidget]
                    </td>
                </tr>
            </tbody>
        </table>
        <span></span>
    </div>

    <div id="searchTabsToolbar" class="toolbarTabs" style="border-bottom: 1px solid var(--color-border-default); display: none">
        <ul id="searchTabs" class="tab-menu"></ul>
        <div class="clear"></div>
    </div>

    <div id="searchResultsFilters" style="padding-top: 10px; display: none">
        <input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none" />

        <span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span>

        <div style="display: inline-block; float: right;">
            <label for="searchInTorrentName" style="margin-left: 15px;">QBT_TR(Search in:)QBT_TR[CONTEXT=SearchEngineWidget]</label>
            <select id="searchInTorrentName" onchange="qBittorrent.Search.searchInTorrentName()">
                <option value="names">QBT_TR(Torrent names only)QBT_TR[CONTEXT=SearchEngineWidget]</option>
                <option value="everywhere">QBT_TR(Everywhere)QBT_TR[CONTEXT=SearchEngineWidget]</option>
            </select>

            <img id="searchResultsGranularFiltersWarning" src="images/dialog-warning.svg" title="QBT_TR(Increase window width to display additional filters)QBT_TR[CONTEXT=SearchEngineWidget]" alt="QBT_TR(Warning)QBT_TR[CONTEXT=SearchEngineWidget]" width="24" height="24">

            <div id="searchResultsGranularFilters">
                <span style="margin-left: 15px;">QBT_TR(Seeds:)QBT_TR[CONTEXT=SearchEngineWidget]</span>
                <input type="number" min="0" max="1000" id="searchMinSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()">
                <span>QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</span>
                <input type="number" min="0" max="1000" id="searchMaxSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()">

                <span style="margin-left: 15px;">QBT_TR(Size:)QBT_TR[CONTEXT=SearchEngineWidget]</span>
                <input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMinSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()">
                <select id="searchMinSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()">
                    <option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
                    <option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="2" selected>QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="3">QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="4">QBT_TR(TiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option>
                </select>
                <span>QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</span>
                <input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMaxSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()">
                <select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()">
                    <option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
                    <option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="2">QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="3" selected>QBT_TR(GiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="4">QBT_TR(TiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option>
                    <option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option>
                </select>
            </div>
        </div>
    </div>

    <div id="searchResultsTableContainer" style="display: none">
        <div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
            <table class="dynamicTable unselectable" style="position:relative;">
                <thead>
                    <tr class="dynamicTableHeader"></tr>
                </thead>
            </table>
        </div>
        <div id="searchResultsTableDiv" class="dynamicTableDiv">
            <table class="dynamicTable unselectable">
                <thead>
                    <tr class="dynamicTableHeader"></tr>
                </thead>
                <tbody></tbody>
            </table>
        </div>
    </div>

    <div style="height: 30px; padding-top: 10px;">
        <button type="button" id="manageSearchPlugins" onclick="qBittorrent.Search.manageSearchPlugins()">QBT_TR(Search plugins...)QBT_TR[CONTEXT=SearchEngineWidget]</button>
    </div>
</div>

<ul id="searchResultsTableMenu" class="contextMenu">
    <li><a href="#Download"><img src="images/downloading.svg" alt="QBT_TR(Download)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Download)QBT_TR[CONTEXT=SearchJobWidget]</a></li>
    <li class="separator"><a href="#OpenDescriptionUrl"><img src="images/application-url.svg" alt="QBT_TR(Open description page)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Open description page)QBT_TR[CONTEXT=SearchJobWidget]</a></li>
    <li>
        <a href="#" class="arrow-right"><img src="images/edit-copy.svg" alt="QBT_TR(Copy)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Copy)QBT_TR[CONTEXT=SearchJobWidget]</a>
        <ul>
            <li><a href="#" id="copySearchTorrentName" class="copySearchDataToClipboard"><img src="images/name.svg" alt="QBT_TR(Name)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Name)QBT_TR[CONTEXT=SearchJobWidget]</a></li>
            <li><a href="#" id="copySearchTorrentDownloadLink" class="copySearchDataToClipboard"><img src="images/insert-link.svg" alt="QBT_TR(Download link)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Download link)QBT_TR[CONTEXT=SearchJobWidget]</a></li>
            <li><a href="#" id="copySearchTorrentDescriptionUrl" class="copySearchDataToClipboard"><img src="images/application-url.svg" alt="QBT_TR(Description page URL)QBT_TR[CONTEXT=SearchJobWidget]" /> QBT_TR(Description page URL)QBT_TR[CONTEXT=SearchJobWidget]</a></li>
        </ul>
    </li>
</ul>

<script>
    'use strict';

    if (window.qBittorrent === undefined) {
        window.qBittorrent = {};
    }

    window.qBittorrent.Search = (function() {
        const exports = function() {
            return {
                startStopSearch: startStopSearch,
                manageSearchPlugins: manageSearchPlugins,
                searchPlugins: searchPlugins,
                searchText: searchText,
                searchSeedsFilter: searchSeedsFilter,
                searchSizeFilter: searchSizeFilter,
                init: init,
                getPlugin: getPlugin,
                searchInTorrentName: searchInTorrentName,
                onSearchPatternChanged: onSearchPatternChanged,
                categorySelected: categorySelected,
                pluginSelected: pluginSelected,
                searchSeedsFilterChanged: searchSeedsFilterChanged,
                searchSizeFilterChanged: searchSizeFilterChanged,
                searchSizeFilterPrefixChanged: searchSizeFilterPrefixChanged,
                closeSearchTab: closeSearchTab,
            };
        };

        const searchTabIdPrefix = "Search-";
        let loadSearchPluginsTimer;
        const searchPlugins = [];
        let prevSearchPluginsResponse;
        let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
        let selectedPlugin = "all";
        let prevSelectedPlugin;
        // whether the current search pattern differs from the pattern that the active search was performed with
        let searchPatternChanged = false;

        let searchResultsTable;
        /** @type Map<number, {
         * searchPattern: string,
         * filterPattern: string,
         * seedsFilter: {min: number, max: number},
         * sizeFilter: {min: number, minUnit: number, max: number, maxUnit: number},
         * searchIn: string,
         * rows: [],
         * rowId: number,
         * selectedRowIds: number[],
         * running: boolean,
         * loadResultsTimer: Timer,
         * sort: {column: string, reverse: string},
         * }> **/
        const searchState = new Map();
        const searchText = {
            pattern: "",
            filterPattern: ""
        };
        const searchSeedsFilter = {
            min: 0,
            max: 0
        };
        const searchSizeFilter = {
            min: 0.00,
            minUnit: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
            max: 0.00,
            maxUnit: 3
        };

        const init = function() {
            // load "Search in" preference from local storage
            $('searchInTorrentName').set('value', (LocalPreferences.get('search_in_filter') === "names") ? "names" : "everywhere");
            const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
                targets: '.searchTableRow',
                menu: 'searchResultsTableMenu',
                actions: {
                    Download: downloadSearchTorrent,
                    OpenDescriptionUrl: openSearchTorrentDescriptionUrl
                },
                offsets: {
                    x: -15,
                    y: -53
                }
            });
            searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable();
            searchResultsTable.setup('searchResultsTableDiv', 'searchResultsTableFixedHeaderDiv', searchResultsTableContextMenu);
            getPlugins();

            // listen for changes to searchInNameFilter
            let searchInNameFilterTimer = -1;
            $('searchInNameFilter').addEvent('input', () => {
                clearTimeout(searchInNameFilterTimer);
                searchInNameFilterTimer = setTimeout(() => {
                    searchInNameFilterTimer = -1;

                    const value = $('searchInNameFilter').get("value");
                    searchText.filterPattern = value;
                    searchFilterChanged();
                }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
            });

            new Keyboard({
                defaultEventType: 'keydown',
                events: {
                    'Enter': function(e) {
                        // accept enter key as a click
                        new Event(e).stop();

                        const elem = e.event.srcElement;
                        if (elem.className.contains("searchInputField")) {
                            $('startSearchButton').click();
                            return;
                        }

                        switch (elem.id) {
                            case "manageSearchPlugins":
                                manageSearchPlugins();
                                break;
                        }
                    }
                }
            }).activate();

            // restore search tabs
            const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]'));
            for (const { id, pattern } of searchJobs) {
                createSearchTab(id, pattern);
            }
        };

        const numSearchTabs = function() {
            return $('searchTabs').getElements('li').length;
        };

        const getSearchIdFromTab = function(tab) {
            return Number(tab.id.substring(searchTabIdPrefix.length));
        };

        const createSearchTab = function(searchId, pattern) {
            const newTabId = `${searchTabIdPrefix}${searchId}`;
            const tabElem = new Element('a', {
                text: pattern,
            });
            const closeTabElem = new Element('img', {
                alt: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
                title: 'QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]',
                src: 'images/application-exit.svg',
                width: '8',
                height: '8',
                style: 'padding-right: 7px; margin-bottom: -1px; margin-left: -5px',
                onclick: 'qBittorrent.Search.closeSearchTab(this)',
            });
            closeTabElem.inject(tabElem, 'top');
            tabElem.appendChild(getStatusIconElement('QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/queued.svg'));
            $('searchTabs').appendChild(new Element('li', {
                id: newTabId,
                class: 'selected',
                html: tabElem.outerHTML,
            }));

            // unhide the results elements
            if (numSearchTabs() >= 1) {
                $('searchResultsNoSearches').style.display = "none";
                $('searchResultsFilters').style.display = "block";
                $('searchResultsTableContainer').style.display = "block";
                $('searchTabsToolbar').style.display = "block";
            }

            // reinitialize tabs
            $('searchTabs').getElements('li').removeEvents('click');
            $('searchTabs').getElements('li').addEvent('click', function(e) {
                $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
                setActiveTab(this);
            });

            // select new tab
            setActiveTab($(newTabId));

            searchResultsTable.clear();
            resetFilters();

            searchState.set(searchId, {
                searchPattern: pattern,
                filterPattern: searchText.filterPattern,
                seedsFilter: { min: searchSeedsFilter.min, max: searchSeedsFilter.max },
                sizeFilter: { min: searchSizeFilter.min, minUnit: searchSizeFilter.minUnit, max: searchSizeFilter.max, maxUnit: searchSizeFilter.maxUnit },
                searchIn: getSearchInTorrentName(),
                rows: [],
                rowId: 0,
                selectedRowIds: [],
                running: true,
                loadResultsTimer: null,
                sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
            });
            updateSearchResultsData(searchId);
        };

        const closeSearchTab = function(el) {
            const tab = el.parentElement.parentElement;
            const searchId = getSearchIdFromTab(tab);
            const isTabSelected = tab.hasClass('selected');
            const newTabToSelect = isTabSelected ? tab.nextSibling || tab.previousSibling : null;

            const currentSearchId = getSelectedSearchId();
            const state = searchState.get(currentSearchId);
            // don't bother sending a stop request if already stopped
            if (state && state.running) {
                stopSearch(searchId);
            }

            tab.destroy();

            new Request({
                url: new URI('api/v2/search/delete'),
                method: 'post',
                data: {
                    id: searchId
                },
            }).send();

            const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]'));
            const jobIndex = searchJobs.findIndex((job) => job.id === searchId);
            if (jobIndex >= 0) {
                searchJobs.splice(jobIndex, 1);
                LocalPreferences.set('search_jobs', JSON.stringify(searchJobs));
            }

            if (numSearchTabs() === 0) {
                resetSearchState();
                resetFilters();

                $('numSearchResultsVisible').set('html', 0);
                $('numSearchResultsTotal').set('html', 0);
                $('searchResultsNoSearches').style.display = "block";
                $('searchResultsFilters').style.display = "none";
                $('searchResultsTableContainer').style.display = "none";
                $('searchTabsToolbar').style.display = "none";
            }
            else if (isTabSelected && newTabToSelect) {
                setActiveTab(newTabToSelect);
                $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
            }
        };

        const saveCurrentTabState = function() {
            const currentSearchId = getSelectedSearchId();
            if (!currentSearchId)
                return;

            const state = searchState.get(currentSearchId);
            if (!state)
                return;

            state.filterPattern = searchText.filterPattern;
            state.seedsFilter = {
                min: searchSeedsFilter.min,
                max: searchSeedsFilter.max,
            };
            state.sizeFilter = {
                min: searchSizeFilter.min,
                minUnit: searchSizeFilter.minUnit,
                max: searchSizeFilter.max,
                maxUnit: searchSizeFilter.maxUnit,
            };
            state.searchIn = getSearchInTorrentName();

            state.sort = {
                column: searchResultsTable.sortedColumn,
                reverse: searchResultsTable.reverseSort,
            };

            // we must copy the array to avoid taking a reference to it
            state.selectedRowIds = [...searchResultsTable.selectedRows];
        };

        const setActiveTab = function(tab) {
            const searchId = getSearchIdFromTab(tab);
            if (searchId === getSelectedSearchId())
                return;

            saveCurrentTabState();

            MochaUI.selected(tab, 'searchTabs');

            const state = searchState.get(searchId);
            let rowsToSelect = [];

            // restore table rows
            searchResultsTable.clear();
            if (state) {
                for (const row of state.rows) {
                    searchResultsTable.updateRowData(row);
                }

                rowsToSelect = state.selectedRowIds;

                // restore filters
                searchText.pattern = state.searchPattern;
                searchText.filterPattern = state.filterPattern;
                $('searchInNameFilter').set("value", state.filterPattern);

                searchSeedsFilter.min = state.seedsFilter.min;
                searchSeedsFilter.max = state.seedsFilter.max;
                $('searchMinSeedsFilter').set('value', state.seedsFilter.min);
                $('searchMaxSeedsFilter').set('value', state.seedsFilter.max);

                searchSizeFilter.min = state.sizeFilter.min;
                searchSizeFilter.minUnit = state.sizeFilter.minUnit;
                searchSizeFilter.max = state.sizeFilter.max;
                searchSizeFilter.maxUnit = state.sizeFilter.maxUnit;
                $('searchMinSizeFilter').set('value', state.sizeFilter.min);
                $('searchMinSizePrefix').set('value', state.sizeFilter.minUnit);
                $('searchMaxSizeFilter').set('value', state.sizeFilter.max);
                $('searchMaxSizePrefix').set('value', state.sizeFilter.maxUnit);

                const currentSearchPattern = $('searchPattern').getProperty('value').trim();
                if (state.running && (state.searchPattern === currentSearchPattern)) {
                    // allow search to be stopped
                    $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
                    searchPatternChanged = false;
                }

                searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse);

                $('searchInTorrentName').set('value', state.searchIn);
            }

            // must restore all filters before calling updateTable
            searchResultsTable.updateTable();
            searchResultsTable.altRow();

            // must reselect rows after calling updateTable
            if (rowsToSelect.length > 0) {
                searchResultsTable.reselectRows(rowsToSelect);
            }

            $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
            $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);

            setupSearchTableEvents(true);
        };

        const getStatusIconElement = function(text, image) {
            return new Element('img', {
                alt: text,
                title: text,
                src: image,
                class: 'statusIcon',
                width: '10',
                height: '10',
                style: 'margin-bottom: -2px; margin-left: 7px',
            });
        };

        const updateStatusIconElement = function(searchId, text, image) {
            const searchTab = $(`${searchTabIdPrefix}${searchId}`);
            if (searchTab) {
                const statusIcon = searchTab.getElement('.statusIcon');
                statusIcon.set('alt', text);
                statusIcon.set('title', text);
                statusIcon.set('src', image);
            }
        };

        const startSearch = function(pattern, category, plugins) {
            searchPatternChanged = false;

            const url = new URI('api/v2/search/start');
            new Request.JSON({
                url: url,
                method: 'post',
                data: {
                    pattern: pattern,
                    category: category,
                    plugins: plugins
                },
                onSuccess: function(response) {
                    $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
                    const searchId = response.id;
                    createSearchTab(searchId, pattern);

                    const searchJobs = JSON.parse(LocalPreferences.get('search_jobs', '[]'));
                    searchJobs.push({ id: searchId, pattern: pattern });
                    LocalPreferences.set('search_jobs', JSON.stringify(searchJobs));
                }
            }).send();
        };

        const stopSearch = function(searchId) {
            const url = new URI('api/v2/search/stop');
            new Request({
                url: url,
                method: 'post',
                data: {
                    id: searchId
                },
                onSuccess: function(response) {
                    resetSearchState(searchId);
                    // not strictly necessary to do this when the tab is being closed, but there's no harm in it
                    updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
                }
            }).send();
        };

        const getSelectedSearchId = function() {
            const selectedTab = $('searchTabs').getElement('li.selected');
            return selectedTab ? getSearchIdFromTab(selectedTab) : null;
        };

        const startStopSearch = function() {
            const currentSearchId = getSelectedSearchId();
            const state = searchState.get(currentSearchId);
            const isSearchRunning = state && state.running;
            if (!isSearchRunning || searchPatternChanged) {
                const pattern = $('searchPattern').getProperty('value').trim();
                let category = $('categorySelect').getProperty('value');
                const plugins = $('pluginsSelect').getProperty('value');

                if (!pattern || !category || !plugins)
                    return;

                searchText.pattern = pattern;
                startSearch(pattern, category, plugins);
            }
            else {
                stopSearch(currentSearchId);
            }
        };

        const openSearchTorrentDescriptionUrl = function() {
            searchResultsTable.selectedRowsIds().each(function(rowId) {
                window.open(searchResultsTable.rows.get(rowId).full_data.descrLink, "_blank");
            });
        };

        const copySearchTorrentName = function() {
            const names = [];
            searchResultsTable.selectedRowsIds().each(function(rowId) {
                names.push(searchResultsTable.rows.get(rowId).full_data.fileName);
            });
            return names.join("\n");
        };

        const copySearchTorrentDownloadLink = function() {
            const urls = [];
            searchResultsTable.selectedRowsIds().each(function(rowId) {
                urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl);
            });
            return urls.join("\n");
        };

        const copySearchTorrentDescriptionUrl = function() {
            const urls = [];
            searchResultsTable.selectedRowsIds().each(function(rowId) {
                urls.push(searchResultsTable.rows.get(rowId).full_data.descrLink);
            });
            return urls.join("\n");
        };

        const downloadSearchTorrent = function() {
            const urls = [];
            searchResultsTable.selectedRowsIds().each(function(rowId) {
                urls.push(searchResultsTable.rows.get(rowId).full_data.fileUrl);
            });

            // only proceed if at least 1 row was selected
            if (!urls.length)
                return;

            showDownloadPage(urls);
        };

        const manageSearchPlugins = function() {
            const id = 'searchPlugins';
            if (!$(id))
                new MochaUI.Window({
                    id: id,
                    title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
                    loadMethod: 'xhr',
                    contentURL: 'views/searchplugins.html',
                    scrollbars: false,
                    maximizable: false,
                    paddingVertical: 0,
                    paddingHorizontal: 0,
                    width: loadWindowWidth(id, 600),
                    height: loadWindowHeight(id, 360),
                    onResize: function() {
                        saveWindowSize(id);
                    },
                    onBeforeBuild: function() {
                        loadSearchPlugins();
                    },
                    onClose: function() {
                        clearTimeout(loadSearchPluginsTimer);
                    }
                });
        };

        const loadSearchPlugins = function() {
            getPlugins();
            loadSearchPluginsTimer = loadSearchPlugins.delay(2000);
        };

        const onSearchPatternChanged = function() {
            const currentSearchId = getSelectedSearchId();
            const state = searchState.get(currentSearchId);
            const currentSearchPattern = $('searchPattern').getProperty('value').trim();
            // start a new search if pattern has changed, otherwise allow the search to be stopped
            if (state && (state.searchPattern === currentSearchPattern)) {
                searchPatternChanged = false;
                $('startSearchButton').set('text', 'QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]');
            }
            else {
                searchPatternChanged = true;
                $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
            }
        };

        const categorySelected = function() {
            selectedCategory = $("categorySelect").get("value");
        };

        const pluginSelected = function() {
            selectedPlugin = $("pluginsSelect").get("value");

            if (selectedPlugin !== prevSelectedPlugin) {
                prevSelectedPlugin = selectedPlugin;
                getSearchCategories();
            }
        };

        const reselectCategory = function() {
            for (let i = 0; i < $("categorySelect").options.length; ++i)
                if ($("categorySelect").options[i].get("value") === selectedCategory)
                    $("categorySelect").options[i].selected = true;

            categorySelected();
        };

        const reselectPlugin = function() {
            for (let i = 0; i < $("pluginsSelect").options.length; ++i)
                if ($("pluginsSelect").options[i].get("value") === selectedPlugin)
                    $("pluginsSelect").options[i].selected = true;

            pluginSelected();
        };

        const resetSearchState = function(searchId) {
            $('startSearchButton').set('text', 'QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]');
            const state = searchState.get(searchId);
            if (state) {
                state.running = false;
                clearTimeout(state.loadResultsTimer);
            }
        };

        const getSearchCategories = function() {
            const populateCategorySelect = function(categories) {
                const categoryHtml = [];
                categories.each(function(category) {
                    const option = new Element("option");
                    option.set("value", category.id);
                    option.set("html", category.name);
                    categoryHtml.push(option.outerHTML);
                });

                // first category is "All Categories"
                if (categoryHtml.length > 1) {
                    // add separator
                    const option = new Element("option");
                    option.set("disabled", true);
                    option.set("html", "──────────");
                    categoryHtml.splice(1, 0, option.outerHTML);
                }

                $('categorySelect').set('html', categoryHtml.join(""));
            };

            const selectedPlugin = $('pluginsSelect').get("value");

            if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) {
                const uniqueCategories = {};
                for (const plugin of searchPlugins) {
                    if ((selectedPlugin === "enabled") && !plugin.enabled)
                        continue;
                    for (const category of plugin.supportedCategories) {
                        if (uniqueCategories[category.id] === undefined) {
                            uniqueCategories[category.id] = category;
                        }
                    }
                }
                // we must sort the ids to maintain consistent order.
                const categories = Object.keys(uniqueCategories).sort().map(id => uniqueCategories[id]);
                populateCategorySelect(categories);
            }
            else {
                const plugin = getPlugin(selectedPlugin);
                const plugins = (plugin === null) ? [] : plugin.supportedCategories;
                populateCategorySelect(plugins);
            }

            reselectCategory();
        };

        const getPlugins = function() {
            new Request.JSON({
                url: new URI('api/v2/search/plugins'),
                method: 'get',
                noCache: true,
                onSuccess: function(response) {
                    if (response !== prevSearchPluginsResponse) {
                        prevSearchPluginsResponse = response;
                        searchPlugins.length = 0;
                        response.forEach(function(plugin) {
                            searchPlugins.push(plugin);
                        });

                        const pluginsHtml = [];
                        pluginsHtml.push('<option value="enabled">QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]</option>');
                        pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>');

                        const searchPluginsEmpty = (searchPlugins.length === 0);
                        if (!searchPluginsEmpty) {
                            $('searchResultsNoPlugins').style.display = "none";
                            if (numSearchTabs() === 0) {
                                $('searchResultsNoSearches').style.display = "block";
                            }

                            // sort plugins alphabetically
                            const allPlugins = searchPlugins.sort((left, right) => {
                                const leftName = left.fullName;
                                const rightName = right.fullName;
                                return window.qBittorrent.Misc.naturalSortCollator.compare(leftName, rightName);
                            });

                            allPlugins.each(function(plugin) {
                                if (plugin.enabled === true)
                                    pluginsHtml.push("<option value='" + window.qBittorrent.Misc.escapeHtml(plugin.name) + "'>" + window.qBittorrent.Misc.escapeHtml(plugin.fullName) + "</option>");
                            });

                            if (pluginsHtml.length > 2)
                                pluginsHtml.splice(2, 0, "<option disabled>──────────</option>");
                        }

                        $('pluginsSelect').set('html', pluginsHtml.join(""));

                        $('searchPattern').setProperty('disabled', searchPluginsEmpty);
                        $('categorySelect').setProperty('disabled', searchPluginsEmpty);
                        $('pluginsSelect').setProperty('disabled', searchPluginsEmpty);
                        $('startSearchButton').setProperty('disabled', searchPluginsEmpty);

                        if (window.qBittorrent.SearchPlugins !== undefined)
                            window.qBittorrent.SearchPlugins.updateTable();

                        reselectPlugin();
                    }
                }
            }).send();
        };

        const getPlugin = function(name) {
            for (let i = 0; i < searchPlugins.length; ++i)
                if (searchPlugins[i].name === name)
                    return searchPlugins[i];

            return null;
        };

        const resetFilters = function() {
            searchText.filterPattern = '';
            $('searchInNameFilter').set('value', '');

            searchSeedsFilter.min = 0;
            searchSeedsFilter.max = 0;
            $('searchMinSeedsFilter').set('value', searchSeedsFilter.min);
            $('searchMaxSeedsFilter').set('value', searchSeedsFilter.max);

            searchSizeFilter.min = 0.00;
            searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
            searchSizeFilter.max = 0.00;
            searchSizeFilter.maxUnit = 3;
            $('searchMinSizeFilter').set('value', searchSizeFilter.min);
            $('searchMinSizePrefix').set('value', searchSizeFilter.minUnit);
            $('searchMaxSizeFilter').set('value', searchSizeFilter.max);
            $('searchMaxSizePrefix').set('value', searchSizeFilter.maxUnit);
        };

        const getSearchInTorrentName = function() {
            return $('searchInTorrentName').get('value') === "names" ? "names" : "everywhere";
        };

        const searchInTorrentName = function() {
            LocalPreferences.set('search_in_filter', getSearchInTorrentName());
            searchFilterChanged();
        };

        const searchSeedsFilterChanged = function() {
            searchSeedsFilter.min = $('searchMinSeedsFilter').get('value');
            searchSeedsFilter.max = $('searchMaxSeedsFilter').get('value');

            searchFilterChanged();
        };

        const searchSizeFilterChanged = function() {
            searchSizeFilter.min = $('searchMinSizeFilter').get('value');
            searchSizeFilter.minUnit = $('searchMinSizePrefix').get('value');
            searchSizeFilter.max = $('searchMaxSizeFilter').get('value');
            searchSizeFilter.maxUnit = $('searchMaxSizePrefix').get('value');

            searchFilterChanged();
        };

        const searchSizeFilterPrefixChanged = function() {
            if ((Number($('searchMinSizeFilter').get('value')) !== 0) || (Number($('searchMaxSizeFilter').get('value')) !== 0))
                searchSizeFilterChanged();
        };

        const searchFilterChanged = function() {
            searchResultsTable.updateTable();
            $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
        };

        const setupSearchTableEvents = function(enable) {
            if (enable)
                $$(".searchTableRow").each(function(target) {
                    target.addEventListener('dblclick', downloadSearchTorrent, false);
                });
            else
                $$(".searchTableRow").each(function(target) {
                    target.removeEventListener('dblclick', downloadSearchTorrent, false);
                });
        };

        const loadSearchResultsData = function(searchId) {
            const state = searchState.get(searchId);

            const maxResults = 500;
            const url = new URI('api/v2/search/results');
            new Request.JSON({
                url: url,
                method: 'get',
                noCache: true,
                data: {
                    id: searchId,
                    limit: maxResults,
                    offset: state.rowId
                },
                onFailure: function(response) {
                    if ((response.status === 400) || (response.status === 404)) {
                        // bad params. search id is invalid
                        resetSearchState(searchId);
                        updateStatusIconElement(searchId, 'QBT_TR(An error occurred during search...)QBT_TR[CONTEXT=SearchJobWidget]', 'images/error.svg');
                    }
                    else {
                        clearTimeout(state.loadResultsTimer);
                        state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId);
                    }
                },
                onSuccess: function(response) {
                    $('error_div').set('html', '');

                    const state = searchState.get(searchId);
                    // check if user stopped the search prior to receiving the response
                    if (!state.running) {
                        clearTimeout(state.loadResultsTimer);
                        updateStatusIconElement(searchId, 'QBT_TR(Search aborted)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-reject.svg');
                        return;
                    }

                    if (response) {
                        setupSearchTableEvents(false);

                        const state = searchState.get(searchId);
                        const newRows = [];

                        if (response.results) {
                            const results = response.results;
                            for (let i = 0; i < results.length; ++i) {
                                const result = results[i];
                                const row = {
                                    rowId: state.rowId,
                                    descrLink: result.descrLink,
                                    fileName: result.fileName,
                                    fileSize: result.fileSize,
                                    fileUrl: result.fileUrl,
                                    nbLeechers: result.nbLeechers,
                                    nbSeeders: result.nbSeeders,
                                    siteUrl: result.siteUrl,
                                    pubDate: result.pubDate,
                                };

                                newRows.push(row);
                                state.rows.push(row);
                                state.rowId += 1;
                            }
                        }

                        // only update table if this search is currently being displayed
                        if (searchId === getSelectedSearchId()) {
                            for (const row of newRows) {
                                searchResultsTable.updateRowData(row);
                            }

                            $('numSearchResultsVisible').set('html', searchResultsTable.getFilteredAndSortedRows().length);
                            $('numSearchResultsTotal').set('html', searchResultsTable.getRowIds().length);

                            searchResultsTable.updateTable();
                            searchResultsTable.altRow();
                        }

                        if ((response.status === "Stopped") && (state.rowId >= response.total)) {
                            resetSearchState(searchId);
                            updateStatusIconElement(searchId, 'QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]', 'images/task-complete.svg');
                            return;
                        }

                        setupSearchTableEvents(true);
                    }

                    clearTimeout(state.loadResultsTimer);
                    state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId);
                }
            }).send();
        };

        const updateSearchResultsData = function(searchId) {
            const state = searchState.get(searchId);
            clearTimeout(state.loadResultsTimer);
            state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId);
        };

        new ClipboardJS('.copySearchDataToClipboard', {
            text: function(trigger) {
                switch (trigger.id) {
                    case "copySearchTorrentName":
                        return copySearchTorrentName();
                    case "copySearchTorrentDownloadLink":
                        return copySearchTorrentDownloadLink();
                    case "copySearchTorrentDescriptionUrl":
                        return copySearchTorrentDescriptionUrl();
                    default:
                        return "";
                }
            }
        });

        return exports();
    })();

    Object.freeze(window.qBittorrent.Search);
</script>
