/**
 * V-Shell (Vertical Workspaces)
 * search.js
 *
 * @author     GdH <G-dH@github.com>
 * @copyright  2022 - 2023
 * @license    GPL-3.0
 *
 */

'use strict';

const Clutter = imports.gi.Clutter;
const Shell = imports.gi.Shell;
const St = imports.gi.St;

const AppDisplay = imports.ui.appDisplay;
const IconGrid = imports.ui.iconGrid;
const Main = imports.ui.main;
const Search = imports.ui.search;

let Me;
// gettext
let _;
let opt;


let SEARCH_MAX_WIDTH;

var SearchModule = class {
    constructor(me) {
        Me = me;
        opt = Me.opt;
        _  = Me.gettext;

        this._firstActivation = true;
        this.moduleEnabled = false;
        this._overrides = null;
    }

    cleanGlobals() {
        Me = null;
        opt = null;
        _ = null;
    }

    update(reset) {
        this.moduleEnabled = opt.get('searchModule');
        const conflict = false;

        reset = reset || !this.moduleEnabled || conflict;

        // don't touch the original code if module disabled
        if (reset && !this._firstActivation) {
            this._disableModule();
        } else if (!reset) {
            this._firstActivation = false;
            this._activateModule();
        }
        if (reset && this._firstActivation)
            console.debug('  SearchModule - Keeping untouched');
    }

    _activateModule() {
        this._updateSearchViewWidth();

        if (!this._overrides)
            this._overrides = new Me.Util.Overrides();

        this._overrides.addOverride('AppSearchProvider', AppDisplay.AppSearchProvider.prototype, AppSearchProvider);
        this._overrides.addOverride('SearchResult', Search.SearchResult.prototype, SearchResult);
        this._overrides.addOverride('SearchResultsView', Search.SearchResultsView.prototype, SearchResultsView);
        this._overrides.addOverride('ProviderInfo', Search.ProviderInfo.prototype, ProviderInfo);

        // Don't expand the search view vertically and align it to the top
        // this is important in the static workspace mode when the search view bg is not transparent
        // also the "Searching..." and "No Results" notifications will be closer to the search entry, with the distance given by margin-top in the stylesheet
        Main.overview._overview._controls.layoutManager._searchController.y_align = Clutter.ActorAlign.START;
        console.debug('  SearchModule - Activated');
    }

    _disableModule() {
        const reset = true;
        this._updateSearchViewWidth(reset);

        if (this._overrides)
            this._overrides.removeAll();
        this._overrides = null;

        Main.overview._overview._controls.layoutManager._searchController.y_align = Clutter.ActorAlign.FILL;


        console.debug('  WorkspaceSwitcherPopupModule - Disabled');
    }

    _updateSearchViewWidth(reset = false) {
        const searchContent = Main.overview._overview._controls.layoutManager._searchController._searchResults._content;
        if (!SEARCH_MAX_WIDTH) { // just store original value;
            const themeNode = searchContent.get_theme_node();
            const width = themeNode.get_max_width();
            SEARCH_MAX_WIDTH = width;
        }

        if (reset) {
            searchContent.set_style('');
        } else {
            let width = Math.round(SEARCH_MAX_WIDTH * opt.SEARCH_VIEW_SCALE);
            searchContent.set_style(`max-width: ${width}px;`);
        }
    }
};

// AppDisplay.AppSearchProvider
const AppSearchProvider = {
    getInitialResultSet(terms, callback, cancellable) {
        // Defer until the parental controls manager is initialized, so the
        // results can be filtered correctly.
        if (!this._parentalControlsManager.initialized) {
            if (Me.shellVersion < 43) {
                let initializedId = this._parentalControlsManager.connect('app-filter-changed', () => {
                    if (this._parentalControlsManager.initialized) {
                        this._parentalControlsManager.disconnect(initializedId);
                        this.getInitialResultSet(terms, callback, cancellable);
                    }
                });
                return null;
            } else {
                // callback has been removed in 43
                cancellable = callback;
                return new Promise(resolve => {
                    let initializedId = this._parentalControlsManager.connect('app-filter-changed', async () => {
                        if (this._parentalControlsManager.initialized) {
                            this._parentalControlsManager.disconnect(initializedId);
                            resolve(await this.getInitialResultSet(terms, cancellable));
                        }
                    });
                });
            }
        }

        const pattern = terms.join(' ');

        let appInfoList = Shell.AppSystem.get_default().get_installed();

        let weightList = {};
        appInfoList = appInfoList.filter(appInfo => {
            try {
                appInfo.get_id(); // catch invalid file encodings
            } catch (e) {
                return false;
            }

            let string = '';
            let name;
            let shouldShow = false;
            if (appInfo.get_display_name) {
                // show only launchers that should be visible in this DE
                shouldShow = appInfo.should_show() && this._parentalControlsManager.shouldShowApp(appInfo);

                if (shouldShow) {
                    let id = appInfo.get_id().split('.');
                    id = id[id.length - 2] || '';
                    let baseName = appInfo.get_string('Name') || '';
                    let dispName = appInfo.get_display_name() || '';
                    let gName = appInfo.get_generic_name() || '';
                    let description = appInfo.get_description() || '';
                    let categories = appInfo.get_string('Categories') || '';
                    let keywords = appInfo.get_string('Keywords') || '';
                    name = `${dispName} ${id}`;
                    string = `${dispName} ${gName} ${baseName} ${description} ${categories} ${keywords} ${id}`;
                }
            }

            let m = -1;
            if (shouldShow && opt.SEARCH_FUZZY) {
                m = Me.Util.fuzzyMatch(pattern, name);
                m = (m + Me.Util.strictMatch(pattern, string)) / 2;
            } else if (shouldShow) {
                m = Me.Util.strictMatch(pattern, string);
            }

            if (m !== -1)
                weightList[appInfo.get_id()] = m;

            return shouldShow && (m !== -1);
        });

        appInfoList.sort((a, b) => weightList[a.get_id()] > weightList[b.get_id()]);

        const usage = Shell.AppUsage.get_default();
        // sort apps by usage list
        appInfoList.sort((a, b) => usage.compare(a.get_id(), b.get_id()));
        // prefer apps where any word in their name starts with the pattern
        appInfoList.sort((a, b) => Me.Util.isMoreRelevant(a.get_display_name(), b.get_display_name(), pattern));

        let results = appInfoList.map(app => app.get_id());

        results = results.concat(this._systemActions.getMatchingActions(terms));

        if (Me.shellVersion < 43) {
            callback(results);
            return null;
        } else {
            return new Promise(resolve => resolve(results));
        }
    },

    // App search result size
    createResultObject(resultMeta) {
        if (resultMeta.id.endsWith('.desktop')) {
            const icon = new AppDisplay.AppIcon(this._appSys.lookup_app(resultMeta['id']), {
                expandTitleOnHover: false,
            });
            icon.icon.setIconSize(opt.SEARCH_ICON_SIZE);
            return icon;
        } else {
            const icon = new AppDisplay.SystemActionIcon(this, resultMeta);
            icon.icon._setSizeManually = true;
            icon.icon.setIconSize(opt.SEARCH_ICON_SIZE);
            return icon;
        }
    },
};

const SearchResult = {
    activate() {
        this.provider.activateResult(this.metaInfo.id, this._resultsView.terms);

        if (this.metaInfo.clipboardText) {
            St.Clipboard.get_default().set_text(
                St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText);
        }
        // don't close overview if Shift key is pressed - Shift moves windows to the workspace
        if (!Me.Util.isShiftPressed())
            Main.overview.toggle();
    },
};

const SearchResultsView = {
    _doSearch() {
        if (!this._doProviderSearch) {
            this._doSearchLegacy();
            return;
        }
        this._startingSearch = false;

        let previousResults = this._results;
        this._results = {};

        this._providers.forEach(provider => {
            const onlyVShellProviders = this._terms.includes('wq//') || this._terms.includes('fq//');
            if (!onlyVShellProviders || (onlyVShellProviders && (provider.id.includes('open-windows') || provider.id.includes('recent-files')))) {
                let previousProviderResults = previousResults[provider.id];
                this._doProviderSearch(provider, previousProviderResults);
            }
        });

        this._updateSearchProgress();

        this._clearSearchTimeout();
    },

    _doSearchLegacy() {
        this._startingSearch = false;

        let previousResults = this._results;
        this._results = {};

        this._providers.forEach(provider => {
            const onlyVShellProviders = this._terms.includes('wq//') || this._terms.includes('fq//');
            if (!onlyVShellProviders || (onlyVShellProviders && (provider.id.includes('open-windows') || provider.id.includes('recent-files')))) {
                provider.searchInProgress = true;

                let previousProviderResults = previousResults[provider.id];
                if (this._isSubSearch && previousProviderResults) {
                    provider.getSubsearchResultSet(previousProviderResults,
                        this._terms,
                        results => {
                            this._gotResults(results, provider);
                        },
                        this._cancellable);
                } else {
                    provider.getInitialResultSet(this._terms,
                        results => {
                            this._gotResults(results, provider);
                        },
                        this._cancellable);
                }
            }
        });

        this._updateSearchProgress();

        this._clearSearchTimeout();
    },

    _updateSearchProgress() {
        let haveResults = this._providers.some(provider => {
            let display = provider.display;
            return display.getFirstResult() !== null;
        });

        this._scrollView.visible = haveResults;
        this._statusBin.visible = !haveResults;

        if (!haveResults) {
            if (this.searchInProgress)
                this._statusText.set_text(_('Searching…'));
            else
                this._statusText.set_text(_('No results.'));
        }
    },
};

// fixes app is null error if search provider id is not a desktop app id.
const ProviderInfo = {
    animateLaunch() {
        let appSys = Shell.AppSystem.get_default();
        let app = appSys.lookup_app(this.provider.appInfo.get_id());
        if (app && app.state === Shell.AppState.STOPPED)
            IconGrid.zoomOutActor(this._content);
    },
};
