/**
 * Vertical Workspaces
 * windowSearchProvider.js
 *
 * @author     GdH <G-dH@github.com>
 * @copyright  2022 -2023
 * @license    GPL-3.0
 */

'use strict';

const { GLib, GObject, Gio, Gtk, Meta, St, Shell } = imports.gi;

const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Settings = Me.imports.settings;
const _ = Me.imports.settings._;

const shellVersion = Settings.shellVersion;

const ModifierType = imports.gi.Clutter.ModifierType;

let windowSearchProvider;
let _enableTimeoutId = 0;

// prefix helps to eliminate results from other search providers
// so it needs to be something less common
// needs to be accessible from vw module
var prefix = 'wq//';

let opt;

const Action = {
    NONE: 0,
    CLOSE: 1,
    CLOSE_ALL: 2,
    MOVE_TO_WS: 3,
    MOVE_ALL_TO_WS: 4
}

function init() {
}

function getOverviewSearchResult() {
        return Main.overview._overview.controls._searchController._searchResults;
}

function update(reset = false) {
    opt = Me.imports.settings.opt;
    if (!reset && opt.WINDOW_SEARCH_PROVIDER_ENABLED && !windowSearchProvider) {
        enable();
    } else if (reset || !opt.WINDOW_SEARCH_PROVIDER_ENABLED) {
        disable();
        opt = null;
    }
}

function enable() {
    // delay because Fedora had problem to register a new provider soon after Shell restarts
    _enableTimeoutId = GLib.timeout_add(
        GLib.PRIORITY_DEFAULT,
        2000,
        () => {
            if (windowSearchProvider == null) {
                windowSearchProvider = new WindowSearchProvider(opt);
                getOverviewSearchResult()._registerProvider(
                    windowSearchProvider
                );
            }
            _enableTimeoutId = 0;
            return GLib.SOURCE_REMOVE;
        }
    );
}

function disable() {
    if (windowSearchProvider) {
        getOverviewSearchResult()._unregisterProvider(
            windowSearchProvider
        );
        windowSearchProvider = null;
    }
    if (_enableTimeoutId) {
        GLib.source_remove(_enableTimeoutId);
        _enableTimeoutId = 0;
    }
}

function fuzzyMatch(term, text) {
    let pos = -1;
    const matches = [];
    // convert all accented chars to their basic form and to lower case
    const _text = text;//.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
    const _term =  term.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();

    // if term matches the substring exactly, gains the highest weight
   if (_text.includes(_term)) {
      return 0;
    }

    for (let i = 0; i < _term.length; i++) {
        let c = _term[i];
        let p;
        if (pos > 0)
            p = _term[i - 1];
        while (true) {
            pos += 1;
            if (pos >= _text.length) {
                return -1;
            }
            if (_text[pos] == c) {
                matches.push(pos);
                break;
            } else if (_text[pos] == p) {
                matches.pop();
                matches.push(pos);
            }
        }
    }

    // add all position to get a weight of the result
    // results closer to the beginning of the text and term characters closer to each other will gain more weight.
    return matches.reduce((r, p) => r + p) - matches.length * matches[0] + matches[0];
}

function strictMatch(term, text) {
    // remove diacritics and accents from letters
    let s = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
    let p = term.toLowerCase();
    let ps = p.split(/ +/);

    // allows to use multiple exact patterns separated by a space in arbitrary order
    for (let w of ps) {  // escape regex control chars
        if (!s.match(w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) {
            return -1;
        }
    }
    return 0;
}

function makeResult(window, i) {
    const app = Shell.WindowTracker.get_default().get_window_app(window);
    const appName = app ? app.get_name() : 'Unknown';
    const windowTitle = window.get_title();
    const wsIndex = window.get_workspace().index();

    return {
      'id': i,
      // convert all accented chars to their basic form and lower case for search
      'name': `${wsIndex + 1}: ${windowTitle} ${appName}`.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(),
      'appName': appName,
      'windowTitle': windowTitle,
      'window': window
    }
}

const closeSelectedRegex = /^\/x!$/;
const closeAllResultsRegex = /^\/xa!$/;
const moveToWsRegex = /^\/m[0-9]+$/;
const moveAllToWsRegex = /^\/ma[0-9]+$/;

var WindowSearchProvider = class WindowSearchProvider {
    constructor(gOptions) {
        this._gOptions = gOptions;
        this.appInfo = Gio.AppInfo.create_from_commandline('true', 'Open Windows', null);
        this.appInfo.get_description = () => 'List of open windows';
        this.appInfo.get_name = () => 'Open Windows';
        this.appInfo.get_id = () => `${Me.metadata.uuid} ${this.title}`;
        this.appInfo.get_icon = () => Gio.icon_new_for_string('focus-windows-symbolic');
        this.appInfo.should_show = () => true;
        this.title = 'Window Search Provider',
        this.canLaunchSearch = true;
        this.isRemoteProvider = false;

        this.action = 0;
    }

    _getResultSet (terms) {
        // do not modify original terms
        let termsCopy = [...terms];
        // search for terms without prefix
        termsCopy[0] = termsCopy[0].replace(prefix, '');

        /*if (gOptions.get('searchWindowsCommands')) {
            this.action = 0;
            this.targetWs = 0;

            const lastTerm = terms[terms.length - 1];
            if (lastTerm.match(closeSelectedRegex)) {
                this.action = Action.CLOSE;
            } else if (lastTerm.match(closeAllResultsRegex)) {
                this.action = Action.CLOSE_ALL;
            } else if (lastTerm.match(moveToWsRegex)) {
                this.action = Action.MOVE_TO_WS;
            } else if (lastTerm.match(moveAllToWsRegex)) {
                this.action = Action.MOVE_ALL_TO_WS;
            }
            if (this.action) {
                terms.pop();
                if (this.action === Action.MOVE_TO_WS || this.action === Action.MOVE_ALL_TO_WS) {
                    this.targetWs = parseInt(lastTerm.replace(/^[^0-9]+/, '')) - 1;
                }
            } else if (lastTerm.startsWith('/')) {
                terms.pop();
            }
        }*/

        const candidates = this.windows;
        const _terms = [].concat(termsCopy);
        let match;

        const term = _terms.join(' ');
        match = (s) => {
            return fuzzyMatch(term, s);
        }

        const results = [];
        let m;
        for (let key in candidates) {
            if (this._gOptions.get('searchFuzzy')) {
                m = fuzzyMatch(term, candidates[key].name);
            } else {
                m = strictMatch(term, candidates[key].name);
            }
            if (m !== -1) {
                results.push({ weight: m, id: key });
            }
        }

        results.sort((a, b) => a.weight > b.weight);
        const currentWs = global.workspace_manager.get_active_workspace_index();
        // prefer current workspace
        results.sort((a, b) => (this.windows[a.id].window.get_workspace().index() !== currentWs) && (this.windows[b.id].window.get_workspace().index() === currentWs));
        results.sort((a, b) => ((_terms != ' ') && (a.weight > 0 && b.weight === 0)));

        this.resultIds = results.map((item) => item.id);
        return this.resultIds;
    }

    getResultMetas (resultIds, callback = null) {
        const metas = resultIds.map((id) => this.getResultMeta(id));
        if (shellVersion >= 43) {
            return new Promise(resolve => resolve(metas));
        } else {
            callback(metas);
        }
    }

    getResultMeta (resultId) {
        const result = this.windows[resultId];
        const wsIndex = result.window.get_workspace().index();
        const app = Shell.WindowTracker.get_default().get_window_app(result.window);
        return {
            'id': resultId,
            'name': `${wsIndex + 1}: ${result.windowTitle}`,
            'description': result.appName,
            'createIcon': (size) => {
                return app
                    ? app.create_icon_texture(size)
                    : new St.Icon({ icon_name: 'icon-missing', icon_size: size });
            }
        }
    }

    launchSearch(terms, timeStamp) {
    }

    activateResult (resultId, terms, timeStamp) {
        const [,,state] = global.get_pointer();

        const isCtrlPressed = (state & ModifierType.CONTROL_MASK) != 0;
        const isShiftPressed = (state & ModifierType.SHIFT_MASK) != 0;

        this.action = 0;
        this.targetWs = 0;

        this.targetWs = global.workspaceManager.get_active_workspace().index() + 1;
        if (isShiftPressed && !isCtrlPressed) {
            this.action = Action.MOVE_TO_WS;
        } else if (isShiftPressed && isCtrlPressed) {
            this.action = Action.MOVE_ALL_TO_WS;
        }

        if (!this.action) {
            const result = this.windows[resultId];
            Main.activateWindow(result.window);
            return;
        }

        switch (this.action) {
        case Action.CLOSE:
            this._closeWindows([resultId]);
            break;
        case Action.CLOSE_ALL:
            this._closeWindows(this.resultIds);
            break;
        case Action.MOVE_TO_WS:
            this._moveWindowsToWs(resultId, [resultId]);
            break;
        case Action.MOVE_ALL_TO_WS:
            this._moveWindowsToWs(resultId, this.resultIds);
            break;
        }
    }

    _closeWindows(ids) {
        let time = global.get_current_time();
        for (let i = 0; i < ids.length; i++) {
            this.windows[ids[i]].window.delete(time + i);
        }
        Main.notify('Window Search Provider', `Closed ${ids.length} windows.`);
    }

    _moveWindowsToWs(selectedId, resultIds) {
        const workspace = global.workspaceManager.get_active_workspace();

        for (let i = 0; i < resultIds.length; i++) {
            this.windows[resultIds[i]].window.change_workspace(workspace);
        }
        const selectedWin = this.windows[selectedId].window;
        selectedWin.activate_with_workspace(global.get_current_time(), workspace);
    }

    getInitialResultSet (terms, callback, cancellable = null) {
        if (shellVersion >=43) {
            cancellable = callback;
        }
        let windows;
        this.windows = windows = {};
        global.display.get_tab_list(Meta.TabList.NORMAL, null).filter(w => w.get_workspace() !== null).map(
            (v, i) => windows[`${i}-${v.get_id()}`] = makeResult(v, `${i}-${v.get_id()}`)
        );



        if (shellVersion >= 43) {
            return new Promise(resolve => resolve(this._getResultSet(terms)));
        } else {
            callback(this._getResultSet(terms));
        }
    }

    filterResults (results, maxResults) {
        //return results.slice(0, maxResults);
        return results;
    }

    getSubsearchResultSet (previousResults, terms, callback, cancellable) {
        // if we return previous results, quick typers get non-actual results
        callback(this._getResultSet(terms));
    }

    createResultOjbect(resultMeta) {
        const app = Shell.WindowTracker.get_default().get_window_app(resultMeta.id);
        return new AppIcon(app);
    }
}
