/**
 * Vertical Workspaces
 * recentFilesSearchProvider.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 recentFilesSearchProvider;
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 = 'fq//';

var 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.RECENT_FILES_SEARCH_PROVIDER_ENABLED && !recentFilesSearchProvider) {
        enable();
    } else if (reset || !opt.RECENT_FILES_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 (recentFilesSearchProvider == null) {
                recentFilesSearchProvider = new RecentFilesSearchProvider(opt);
                getOverviewSearchResult()._registerProvider(recentFilesSearchProvider);
            }
            _enableTimeoutId = 0;
            return GLib.SOURCE_REMOVE;
        }
    );
}

function disable() {
    if (recentFilesSearchProvider) {
        getOverviewSearchResult()._unregisterProvider(recentFilesSearchProvider);
        recentFilesSearchProvider = 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 RecentFilesSearchProvider = class RecentFilesSearchProvider {
    constructor(gOptions) {
        this._gOptions = gOptions;
        this.appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/nautilus -ws recent:///', 'Recent Files', null);
        //this.appInfo = Shell.AppSystem.get_default().lookup_app('org.gnome.Nautilus.desktop').appInfo;
        this.appInfo.get_description = () => _('Search recent files');
        this.appInfo.get_name = () => _('Recent Files');
        this.appInfo.get_id = () => 'org.gnome.Nautilus.desktop';
        this.appInfo.get_icon = () => Gio.icon_new_for_string('document-open-recent-symbolic');
        this.appInfo.should_show = () => true;
        this.title = _('Recent Files Search Provider'),
        this.canLaunchSearch = true;
        this.isRemoteProvider = false;
    }

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

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

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

        const results = [];
        let m;
        for (let id in candidates) {
            const file = this.files[id];
            const name = `${file.get_age()}d: ${file.get_display_name()} ${file.get_uri_display().replace(`\/${file.get_display_name()}`, '')}`;
            if (this._gOptions.get('searchFuzzy')) {
                m = fuzzyMatch(term, name);
            } else {
                m = strictMatch(term, name);
            }
            if (m !== -1) {
                results.push({ weight: m, id: id});
            }
        }

        results.sort((a, b) => this.files[a.id].get_visited() < this.files[b.id].get_visited());

        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.files[resultId];
        return {
            'id': resultId,
            'name': `${result.get_age()}:  ${result.get_display_name()}`,
            'description': `${result.get_uri_display().replace(`\/${result.get_display_name()}`, '')}`,
            'createIcon': (size) => {
                let icon = this.getIcon(result, size);
                return icon;
            },
        }
    }

    getIcon(result, size) {
        let file = Gio.File.new_for_uri(result.get_uri());
        let info = file.query_info(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH,
                                   Gio.FileQueryInfoFlags.NONE, null);
        let path = info.get_attribute_byte_string(
            Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH);

        let icon, gicon;

        if (path) {
            gicon = Gio.FileIcon.new(Gio.File.new_for_path(path));
        } else {
            const appInfo = Gio.AppInfo.get_default_for_type(result.get_mime_type(), false);
            if (appInfo)
                gicon = appInfo.get_icon();
        }

        if (gicon) {
            icon = new St.Icon({ gicon, icon_size: size });
        } else {
            icon = new St.Icon({ icon_name: 'icon-missing', icon_size: size });
        }

        return icon;
    }

    launchSearch(terms, timeStamp) {
        this._openNautilus('recent:///')
    }

    _openNautilus(uri) {
        try {
            GLib.spawn_command_line_async(`nautilus -ws ${uri}`);
        } catch (e) {
            log(e);
        }
    }

    activateResult (resultId, terms, timeStamp) {
        const file = this.files[resultId];

        const [,,state] = global.get_pointer();
        //const isCtrlPressed = (state & ModifierType.CONTROL_MASK) != 0;
        const isShiftPressed = (state & ModifierType.SHIFT_MASK) != 0;

        if (isShiftPressed) {
            Main.overview.toggle();
            this._openNautilus(file.get_uri());
        } else {
            const appInfo = Gio.AppInfo.get_default_for_type(file.get_mime_type(), false);
            if (!(appInfo && appInfo.launch_uris([file.get_uri()], null))) {
                this._openNautilus(file.get_uri());
            }
        }
    }

    getInitialResultSet (terms, callback, cancellable = null) {
        if (shellVersion >=43) {
            cancellable = callback;
        }

        const filesDict = {};
        const files = Gtk.RecentManager.get_default().get_items().filter((f)=> f.exists());

        for (let file of files) {
            filesDict[file.get_uri()] = file;
        }

        this.files = filesDict;

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

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

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

    createResultOjbect(resultMeta) {
        return this.files[resultMeta.id];
    }
}