/*
 * MIT License
 * Copyright (C) 2024 Thomas Piccirello
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

"use strict";

window.qBittorrent ??= {};
window.qBittorrent.Search ??= (() => {
    const exports = () => {
        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 = -1;
    const searchPlugins = [];
    let prevSearchPluginsResponse;
    let selectedCategory = "QBT_TR(All categories)QBT_TR[CONTEXT=SearchEngineWidget]";
    let selectedPlugin = "enabled";
    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 searchResultsTabsContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
        targets: ".searchTab",
        menu: "searchResultsTabsMenu",
        actions: {
            closeTab: (tab) => { closeSearchTab(tab); },
            closeAllTabs: () => {
                for (const tab of document.querySelectorAll("#searchTabs .searchTab"))
                    closeSearchTab(tab);
            }
        },
        offsets: {
            x: -15,
            y: -53
        },
        onShow: function() {
            setActiveTab(this.options.element);
        }
    });

    const init = () => {
        // load "Search in" preference from local storage
        $("searchInTorrentName").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: 0,
                y: -60
            }
        });
        searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable();
        searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu);
        getPlugins();

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

                const value = $("searchInNameFilter").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
                    e.preventDefault();
                    e.stopPropagation();

                    const elem = e.event.srcElement;
                    if (elem.className.contains("searchInputField")) {
                        document.getElementById("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 = () => {
        return $("searchTabs").getElements("li").length;
    };

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

    const createSearchTab = (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: "10",
            height: "10",
            onclick: "qBittorrent.Search.closeSearchTab(this);",
        });
        closeTabElem.inject(tabElem, "top");

        tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));

        const listItem = document.createElement("li");
        listItem.id = newTabId;
        listItem.classList.add("selected", "searchTab");
        listItem.addEventListener("click", (e) => {
            setActiveTab(listItem);
            document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
        });
        listItem.appendChild(tabElem);
        $("searchTabs").appendChild(listItem);
        searchResultsTabsContextMenu.addTarget(listItem);

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

        // select new tab
        setActiveTab(listItem);

        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: -1,
            sort: { column: searchResultsTable.sortedColumn, reverse: searchResultsTable.reverseSort },
        });
        updateSearchResultsData(searchId);
    };

    const closeSearchTab = (el) => {
        const tab = el.closest("li.searchTab");
        if (!tab)
            return;

        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").textContent = 0;
            $("numSearchResultsTotal").textContent = 0;
            $("searchResultsNoSearches").style.display = "block";
            $("searchResultsFilters").style.display = "none";
            $("searchResultsTableContainer").style.display = "none";
            $("searchTabsToolbar").style.display = "none";
        }
        else if (isTabSelected && newTabToSelect) {
            setActiveTab(newTabToSelect);
            document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
        }
    };

    const saveCurrentTabState = () => {
        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 = (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").value = state.filterPattern;

            searchSeedsFilter.min = state.seedsFilter.min;
            searchSeedsFilter.max = state.seedsFilter.max;
            $("searchMinSeedsFilter").value = state.seedsFilter.min;
            $("searchMaxSeedsFilter").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").value = state.sizeFilter.min;
            $("searchMinSizePrefix").value = state.sizeFilter.minUnit;
            $("searchMaxSizeFilter").value = state.sizeFilter.max;
            $("searchMaxSizePrefix").value = state.sizeFilter.maxUnit;

            const currentSearchPattern = $("searchPattern").value.trim();
            if (state.running && (state.searchPattern === currentSearchPattern)) {
                // allow search to be stopped
                document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
                searchPatternChanged = false;
            }

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

            $("searchInTorrentName").value = state.searchIn;
        }

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

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

        $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
        $("numSearchResultsTotal").textContent = searchResultsTable.getRowSize();

        setupSearchTableEvents(true);
    };

    const getStatusIconElement = (text, image) => {
        return new Element("img", {
            alt: text,
            title: text,
            src: image,
            class: "statusIcon",
            width: "12",
            height: "12",
        });
    };

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

    const startSearch = (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: (response) => {
                document.getElementById("startSearchButton").lastChild.textContent = "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 = (searchId) => {
        const url = new URI("api/v2/search/stop");
        new Request({
            url: url,
            method: "post",
            data: {
                id: searchId
            },
            onSuccess: (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 = () => {
        const selectedTab = $("searchTabs").getElement("li.selected");
        return selectedTab ? getSearchIdFromTab(selectedTab) : null;
    };

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

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

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

    const openSearchTorrentDescriptionUrl = () => {
        for (const rowID of searchResultsTable.selectedRowsIds())
            window.open(searchResultsTable.getRow(rowID).full_data.descrLink, "_blank");
    };

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

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

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

    const downloadSearchTorrent = () => {
        const urls = [];
        for (const rowID of searchResultsTable.selectedRowsIds())
            urls.push(searchResultsTable.getRow(rowID).full_data.fileUrl);

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

        showDownloadPage(urls);
    };

    const manageSearchPlugins = () => {
        const id = "searchPlugins";
        if (!$(id)) {
            new MochaUI.Window({
                id: id,
                title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
                icon: "images/qbittorrent-tray.svg",
                loadMethod: "xhr",
                contentURL: "views/searchplugins.html",
                scrollbars: false,
                maximizable: false,
                paddingVertical: 0,
                paddingHorizontal: 0,
                width: loadWindowWidth(id, 600),
                height: loadWindowHeight(id, 360),
                onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
                    saveWindowSize(id);
                }),
                onBeforeBuild: () => {
                    loadSearchPlugins();
                },
                onClose: () => {
                    clearTimeout(loadSearchPluginsTimer);
                    loadSearchPluginsTimer = -1;
                }
            });
        }
    };

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

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

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

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

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

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

        categorySelected();
    };

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

        pluginSelected();
    };

    const resetSearchState = (searchId) => {
        document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
        const state = searchState.get(searchId);
        if (state) {
            state.running = false;
            clearTimeout(state.loadResultsTimer);
            state.loadResultsTimer = -1;
        }
    };

    const getSearchCategories = () => {
        const populateCategorySelect = (categories) => {
            const categoryOptions = [];

            for (const category of categories) {
                const option = document.createElement("option");
                option.value = category.id;
                option.textContent = category.name;
                categoryOptions.push(option);
            };

            // first category is "All Categories"
            if (categoryOptions.length > 1) {
                // add separator
                const option = document.createElement("option");
                option.disabled = true;
                option.textContent = "──────────";
                categoryOptions.splice(1, 0, option);
            }

            $("categorySelect").replaceChildren(...categoryOptions);
        };

        const selectedPlugin = $("pluginsSelect").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 = () => {
        new Request.JSON({
            url: new URI("api/v2/search/plugins"),
            method: "get",
            noCache: true,
            onSuccess: (response) => {
                const createOption = (text, value, disabled = false) => {
                    const option = document.createElement("option");
                    if (value !== undefined)
                        option.value = value;
                    option.textContent = text;
                    option.disabled = disabled;
                    return option;
                };

                if (response !== prevSearchPluginsResponse) {
                    prevSearchPluginsResponse = response;
                    searchPlugins.length = 0;
                    response.forEach((plugin) => {
                        searchPlugins.push(plugin);
                    });

                    const pluginOptions = [];
                    pluginOptions.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"));
                    pluginOptions.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all"));

                    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((plugin) => {
                            if (plugin.enabled === true)
                                pluginOptions.push(createOption(plugin.fullName, plugin.name));
                        });

                        if (pluginOptions.length > 2)
                            pluginOptions.splice(2, 0, createOption("──────────", undefined, true));
                    }

                    $("pluginsSelect").replaceChildren(...pluginOptions);

                    $("searchPattern").disabled = searchPluginsEmpty;
                    $("categorySelect").disabled = searchPluginsEmpty;
                    $("pluginsSelect").disabled = searchPluginsEmpty;
                    document.getElementById("startSearchButton").disabled = searchPluginsEmpty;

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

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

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

        return null;
    };

    const resetFilters = () => {
        searchText.filterPattern = "";
        $("searchInNameFilter").value = "";

        searchSeedsFilter.min = 0;
        searchSeedsFilter.max = 0;
        $("searchMinSeedsFilter").value = searchSeedsFilter.min;
        $("searchMaxSeedsFilter").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").value = searchSizeFilter.min;
        $("searchMinSizePrefix").value = searchSizeFilter.minUnit;
        $("searchMaxSizeFilter").value = searchSizeFilter.max;
        $("searchMaxSizePrefix").value = searchSizeFilter.maxUnit;
    };

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

    const searchInTorrentName = () => {
        LocalPreferences.set("search_in_filter", getSearchInTorrentName());
        searchFilterChanged();
    };

    const searchSeedsFilterChanged = () => {
        searchSeedsFilter.min = $("searchMinSeedsFilter").value;
        searchSeedsFilter.max = $("searchMaxSeedsFilter").value;

        searchFilterChanged();
    };

    const searchSizeFilterChanged = () => {
        searchSizeFilter.min = $("searchMinSizeFilter").value;
        searchSizeFilter.minUnit = $("searchMinSizePrefix").value;
        searchSizeFilter.max = $("searchMaxSizeFilter").value;
        searchSizeFilter.maxUnit = $("searchMaxSizePrefix").value;

        searchFilterChanged();
    };

    const searchSizeFilterPrefixChanged = () => {
        if ((Number($("searchMinSizeFilter").value) !== 0) || (Number($("searchMaxSizeFilter").value) !== 0))
            searchSizeFilterChanged();
    };

    const searchFilterChanged = () => {
        searchResultsTable.updateTable();
        $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
    };

    const setupSearchTableEvents = (enable) => {
        const clickHandler = (e) => { downloadSearchTorrent(); };
        if (enable) {
            $$(".searchTableRow").each((target) => {
                target.addEventListener("dblclick", clickHandler);
            });
        }
        else {
            $$(".searchTableRow").each((target) => {
                target.removeEventListener("dblclick", clickHandler);
            });
        }
    };

    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").textContent = "";

                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,
                                engineName: result.engineName,
                                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").textContent = searchResultsTable.getFilteredAndSortedRows().length;
                        $("numSearchResultsTotal").textContent = searchResultsTable.getRowSize();

                        searchResultsTable.updateTable();
                    }

                    setupSearchTableEvents(true);

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

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