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

'use strict';

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Meta = imports.gi.Meta;
const St = imports.gi.St;

const Main = imports.ui.main;
const Search = imports.ui.search;
const Workspace = imports.ui.workspace;

const ExtensionUtils = imports.misc.extensionUtils;
const MyExtension = ExtensionUtils.getCurrentExtension();

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

function init() {
    ExtensionUtils.initTranslations();
    return new Extension();
}

class Extension {
    _init() {
        Me = {};

        Me.shellVersion = parseFloat(imports.misc.config.PACKAGE_VERSION);
        Me.imports = MyExtension.imports;
        Me.metadata = MyExtension.metadata;
        Me.gSettings = ExtensionUtils.getSettings(Me.metadata['settings-schema']);
        Me.Settings = MyExtension.imports.lib.settings;
        Me.gettext = imports.gettext.domain(Me.metadata['gettext-domain']).gettext;
        Me.Util = MyExtension.imports.lib.util;

        Me.Modules = this._importModules();
        Me.moduleList = this._getModuleList();

        Me.WSP_PREFIX = Me.Modules.windowSearchProviderModule._PREFIX;
        Me.RFSP_PREFIX = Me.Modules.recentFilesSearchProviderModule._PREFIX;
        Me.ESP_PREFIX = Me.Modules.extensionsSearchProviderModule._PREFIX;

        Me.opt = new Me.Settings.Options(Me);
        _ = Me.gettext;
        opt = Me.opt;

        Me.Util.init(Me);

        Me.moduleList.forEach(module => {
            Me.Modules[module] = new Me.Modules[module](Me);
        });

        Me.repairOverrides = this._repairOverrides;
    }

    _importModules() {
        return {
            appDisplayModule:                   Me.imports.lib.appDisplay.AppDisplayModule,
            appFavoritesModule:                 Me.imports.lib.appFavorites.AppFavoritesModule,
            dashModule:                         Me.imports.lib.dash.DashModule,
            iconGridModule:                     Me.imports.lib.iconGrid.IconGridModule,
            layoutModule:                       Me.imports.lib.layout.LayoutModule,
            messageTrayModule:                  Me.imports.lib.messageTray.MessageTrayModule,
            osdWindowModule:                    Me.imports.lib.osdWindow.OsdWindowModule,
            overviewModule:                     Me.imports.lib.overview.OverviewModule,
            overlayKeyModule:                   Me.imports.lib.overlayKey.OverlayKeyModule,
            overviewControlsModule:             Me.imports.lib.overviewControls.OverviewControlsModule,
            panelModule:                        Me.imports.lib.panel.PanelModule,
            searchModule:                       Me.imports.lib.search.SearchModule,
            searchControllerModule:             Me.imports.lib.searchController.SearchControllerModule,
            swipeTrackerModule:                 Me.imports.lib.swipeTracker.SwipeTrackerModule,
            windowAttentionHandlerModule:       Me.imports.lib.windowAttentionHandler.WindowAttentionHandlerModule,
            windowManagerModule:                Me.imports.lib.windowManager.WindowManagerModule,
            windowPreviewModule:                Me.imports.lib.windowPreview.WindowPreviewModule,
            workspaceAnimationModule:           Me.imports.lib.workspaceAnimation.WorkspaceAnimationModule,
            workspaceModule:                    Me.imports.lib.workspace.WorkspaceModule,
            workspaceSwitcherPopupModule:       Me.imports.lib.workspaceSwitcherPopup.WorkspaceSwitcherPopupModule,
            workspaceThumbnailModule:           Me.imports.lib.workspaceThumbnail.WorkspaceThumbnailModule,
            workspacesViewModule:               Me.imports.lib.workspacesView.WorkspacesViewModule,
            windowSearchProviderModule:         Me.imports.lib.windowSearchProvider.WindowSearchProviderModule,
            winTmbModule:                       Me.imports.lib.winTmb.WinTmbModule,
            recentFilesSearchProviderModule:    Me.imports.lib.recentFilesSearchProvider.RecentFilesSearchProviderModule,
            extensionsSearchProviderModule:     Me.imports.lib.extensionsSearchProvider.ExtensionsSearchProviderModule,
        };
    }

    _getModuleList() {
        return Object.keys(Me.Modules);
    }

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

    enable() {
        this._init();
        // flag for Util.getEnabledExtensions()
        Me.extensionsLoadIncomplete = Main.layoutManager._startingUp;

        this._activateVShell();
        Me.extensionsLoadIncomplete = false;
        console.debug(`${Me.metadata.name}: enabled`);
    }

    // Reason for using "unlock-dialog" session mode:
    // Updating the "appDisplay" content every time the screen is locked/unlocked takes quite a lot of time and affects the user experience.
    disable() {
        this._removeVShell();
        this._disposeModules();

        // If Dash to Dock is enabled, disabling V-Shell can end in broken overview
        Main.overview.hide();

        console.debug(`${Me.metadata.name}: disabled`);

        this._cleanGlobals();
    }

    _disposeModules() {
        Me.opt.destroy();
        Me.opt = null;

        for (let module of Me.moduleList) {
            if (!Me.Modules[module].moduleEnabled)
                Me.Modules[module].cleanGlobals();
        }

        Me.Util.cleanGlobals();
        Me.Modules = null;
    }

    _activateVShell() {
        this._enabled = true;

        this._originalGetNeighbor = Meta.Workspace.prototype.get_neighbor;

        this._removeTimeouts();
        this._timeouts = {};

        // load VShell configuration
        this._updateSettings();

        // activate all enabled VShell modules
        this._updateOverrides();

        // connect signals to help VShell adapt to changes in DE configuration
        this._updateConnections();

        // switch PageUp/PageDown workspace switcher shortcuts
        this._switchPageShortcuts();

        // if Dash to Dock detected force enable "Fix for DtD" option
        this._updateFixDashToDockOption();

        // update overview background wallpaper if enabled, but don't set it too early on session startup
        // because it crashes wayland
        if (!Main.layoutManager._startingUp || Meta.is_restart())
            Main.overview._overview.controls._setBackground();

        this._updateSettingsConnection();

        // store dash _workId so we will be able to detect replacement when entering overview
        this._storeDashId();

        // workaround for upstream bug - overview always shows workspace 1 instead of the active one after restart
        this._setInitialWsIndex();
    }

    _removeVShell() {
        this._enabled = false;

        const reset = true;
        this._removeTimeouts();

        this._removeConnections();
        Main.overview._overview.controls._setBackground(reset);

        // remove changes mede by VShell modules
        this._updateOverrides(reset);

        // switch PageUp/PageDown workspace switcher shortcuts
        this._switchPageShortcuts();

        // remove any position offsets from dash and ws thumbnails
        if (!Me.Util.dashNotDefault()) {
            Main.overview.dash.translation_x = 0;
            Main.overview.dash.translation_y = 0;
        }
        Main.overview._overview._controls._thumbnailsBox.translation_x = 0;
        Main.overview._overview._controls._thumbnailsBox.translation_y = 0;
        Main.overview._overview._controls._searchEntryBin.translation_y = 0;
        Main.overview._overview._controls.set_child_above_sibling(Main.overview._overview._controls._workspacesDisplay, null);
        // restore default animation speed
        St.Settings.get().slow_down_factor = 1;

        // restore default dash background style
        Main.overview.dash._background.set_style('');
        // hide status message if shown
        this._showStatusMessage(false);
        this._prevDash = null;

        Meta.Workspace.prototype.get_neighbor = this._originalGetNeighbor;
    }

    _removeTimeouts() {
        if (this._timeouts) {
            Object.values(this._timeouts).forEach(id => {
                if (id)
                    GLib.source_remove(id);
            });
        }
        this._timeouts = null;
    }

    _storeDashId() {
        const dash = Main.overview.dash;
        this._prevDash = dash._workId;
    }

    _setInitialWsIndex() {
        if (Main.layoutManager._startingUp) {
            GLib.idle_add(GLib.PRIORITY_LOW, () => {
                Main.overview._overview.controls._workspaceAdjustment.set_value(global.workspace_manager.get_active_workspace_index());
            });
        }
    }

    _updateSettingsConnection() {
        if (!opt._extensionUpdateId)
            opt._extensionUpdateId = opt.connect('changed', this._updateSettings.bind(this));
    }

    _updateFixDashToDockOption() {
        const dtdEnabled = !!(Me.Util.getEnabledExtensions('dash-to-dock').length ||
                              Me.Util.getEnabledExtensions('ubuntu-dock').length);

        // force enable Fix Dash to Dock option if DtD detected
        opt._watchDashToDock = dtdEnabled;
        // opt.set('fixUbuntuDock', dtdEnabled);
    }

    _updateConnections() {
        if (!this._monitorsChangedConId)
            this._monitorsChangedConId = Main.layoutManager.connect('monitors-changed', () => this._updateVShell(2000));


        if (!this._showingOverviewConId)
            this._showingOverviewConId = Main.overview.connect('showing', this._onShowingOverview.bind(this));

        if (!this._sessionModeConId) {
            // the panel must be visible when screen is locked
            this._sessionModeConId = Main.sessionMode.connect('updated', session => {
                if (session.currentMode === 'user' || session.parentMode === 'user') {
                    this._timeouts.unlock = GLib.idle_add(GLib.PRIORITY_LOW,
                        () => {
                            Me.Modules.panelModule.update();
                            Me.Modules.overviewControlsModule.update();
                            Me.Modules.winTmbModule.showThumbnails();

                            this._timeouts.unlock = 0;
                            return GLib.SOURCE_REMOVE;
                        }
                    );
                } else if (session.currentMode === 'unlock-dialog') {
                    Me.Modules.panelModule.update(true);
                    Me.Modules.winTmbModule.hideThumbnails();
                }
            });
        }

        if (!this._watchDockSigId) {
            this._watchDockSigId = Main.extensionManager.connect('extension-state-changed',
                (source, extension) => {
                    const uuid = extension.uuid;
                    // ExtensionState = {
                    //     ENABLED: 1,
                    //     DISABLED: 2,
                    //     ERROR: 3,
                    //     OUT_OF_DATE: 4,
                    //     DOWNLOADING: 5,
                    //     INITIALIZED: 6,
                    //     DISABLING: 7,
                    //     ENABLING: 8,
                    //
                    //     // Used as an error state for operations on unknown extensions,
                    //     // should never be in a real extensionMeta object.
                    //     UNINSTALLED: 99,
                    // };
                    // no need to restart on disable/remove
                    //  - if DtD was enabled before VShell, VShell will be rebased by extensionSystem
                    //  - if DtD was enabled after VShell, the first _showingOverview detect replacement of the dash and repair VShell
                    const reset = [1, 2].includes(extension.state);
                    const dashReplacement = uuid.includes('dash-to-dock') || uuid.includes('ubuntu-dock') || uuid.includes('dash-to-panel');
                    if (dashReplacement && reset)
                        opt._watchDashToDock = true;
                    if (!Main.layoutManager._startingUp && reset && dashReplacement)
                        this._updateVShell(1999);
                }
            );
        }
    }

    _removeConnections() {
        if (this._monitorsChangedConId) {
            Main.layoutManager.disconnect(this._monitorsChangedConId);
            this._monitorsChangedConId = 0;
        }

        if (this._showingOverviewConId) {
            Main.overview.disconnect(this._showingOverviewConId);
            this._showingOverviewConId = 0;
        }

        if (this._sessionModeConId) {
            Main.sessionMode.disconnect(this._sessionModeConId);
            this._sessionModeConId = 0;
        }

        if (this._watchDockSigId) {
            Main.extensionManager.disconnect(this._watchDockSigId);
            this._watchDockSigId = 0;
        }
    }

    _updateOverrides(reset = false) {
        Me.Modules.workspacesViewModule.update(reset);
        Me.Modules.workspaceThumbnailModule.update(reset);
        Me.Modules.overviewModule.update(reset);
        Me.Modules.overviewControlsModule.update(reset);

        Me.Modules.workspaceModule.update(reset);
        Me.Modules.windowPreviewModule.update(reset);
        Me.Modules.windowManagerModule.update(reset);

        Me.Modules.layoutModule.update(reset);
        Me.Modules.dashModule.update(reset);
        // avoid enabling panel module when session is locked
        if (reset || (!reset && !Main.sessionMode.isLocked))
            Me.Modules.panelModule.update(reset);
        // the panel must be visible when screen is locked
        // at startup time, panel will be updated from the startupAnimation after allocation
        if (!reset && Main.sessionMode.isLocked && !Main.layoutManager._startingUp)
            Me.Modules.panelModule._showPanel(true);
            // PanelModule._showPanel(true);
            // hide panel so it appears directly on the final place
        /* else if (Main.layoutManager._startingUp && !Meta.is_restart())
            Main.panel.opacity = 0;*/

        Me.Modules.workspaceAnimationModule.update(reset);
        Me.Modules.workspaceSwitcherPopupModule.update(reset);

        Me.Modules.swipeTrackerModule.update(reset);

        Me.Modules.searchModule.update(reset);

        Me.Modules.windowSearchProviderModule.update(reset);
        Me.Modules.recentFilesSearchProviderModule.update(reset);
        Me.Modules.extensionsSearchProviderModule.update(reset);

        // don't rebuild app grid on any screen lock
        // even if the extension includes unlock-screen session mode
        // disable/enable is called at least once even on GS44
        // when screen lock is activated for the first time
        // because every first disable of each extension rebases
        // the entire extensions stack that was enabled later
        if (Main.sessionMode.isLocked)
            this._sessionLockActive = true;

        // This covers unnecessary enable/disable cycles during first screen lock when extensions are rebased, but is not allowed by the EGO rules
        if (!this._sessionLockActive || !Main.extensionManager._getEnabledExtensions().includes(Me.metadata.uuid)) {
            // iconGridModule will be updated from appDisplayModule
            Me.Modules.appDisplayModule.update(reset);
        }

        if (!this._sessionLockActive && !Main.layoutManager._startingUp && opt.APP_GRID_PERFORMANCE) {
            // Avoid showing status at startup, can cause freeze
            this._showStatusMessage();
        }

        if (!Main.sessionMode.isLocked)
            this._sessionLockActive = false;

        // iconGridModule will be updated from appDisplayModule
        // Me.Modules.appDisplayModule.update(reset);

        Me.Modules.windowAttentionHandlerModule.update(reset);
        Me.Modules.appFavoritesModule.update(reset);
        Me.Modules.messageTrayModule.update(reset);
        Me.Modules.osdWindowModule.update(reset);
        Me.Modules.overlayKeyModule.update(reset);
        Me.Modules.searchControllerModule.update(reset);
        Me.Modules.winTmbModule.update(reset);

        if (!reset && !Main.layoutManager._startingUp)
            Main.overview._overview.controls.setInitialTranslations();
    }

    _onShowingOverview() {
        if (Main.layoutManager._startingUp)
            return;

        Main.overview._overview.controls.opacity = 255;

        // store pointer X coordinate for OVERVIEW_MODE 1 window spread - if mouse pointer is steady, don't spread
        opt.showingPointerX = global.get_pointer()[0];

        if (!Main.overview._overview.controls._bgManagers && (opt.SHOW_BG_IN_OVERVIEW || opt.SHOW_WS_PREVIEW_BG))
            Main.overview._overview.controls._setBackground();

        if (opt._watchDashToDock) {
            // workaround for Dash to Dock (Ubuntu Dock) breaking overview allocations after enabled and changed position
            // DtD replaces dock and its _workId on every position change
            const dash = Main.overview.dash;
            if (this._prevDash !== dash._workId)
                this._updateVShell(0);
        }
    }

    _updateVShell(timeout = 200) {
        if (!this._enabled || Main.layoutManager._startingUp)
            return;

        if (this._timeouts.reset)
            GLib.source_remove(this._timeouts.reset);
        this._timeouts.reset = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            timeout,
            () => {
                if (!this._enabled)
                    return GLib.SOURCE_REMOVE;

                const dash = Main.overview.dash;
                if (timeout < 2000) { // timeout < 2000 for partial update
                    this._prevDash = dash._workId;
                    console.warn(`[${Me.metadata.name}]: Dash has been replaced, updating extension ...`);
                    Me._resetInProgress = true;
                    // update only necessary modules if dash has been replaced
                    this._repairOverrides();
                    Me._resetInProgress = false;
                } else {
                    console.warn(`[${Me.metadata.name}]: Updating extension ...`);
                    // for case the monitor configuration has been changed, update all
                    Me._resetInProgress = true;
                    this._activateVShell();
                    Me._resetInProgress = false;
                }
                this._timeouts.reset = 0;
                return GLib.SOURCE_REMOVE;
            }
        );
    }

    // the key modules that can be affected by the supported incompatible extensions
    _repairOverrides() {
        Me.Modules.overviewModule.update();
        Me.Modules.overviewControlsModule.update();
        Me.Modules.workspacesViewModule.update();
        Me.Modules.windowPreviewModule.update();
        Me.Modules.panelModule.update();
        Me.Modules.dashModule.update();
    }

    _updateSettings(settings, key) {
        // update settings cache and option variables
        opt._updateSettings();

        // avoid overload while loading profile - update only once
        // delayed gsettings writes are processed alphabetically
        if (key === 'aaa-loading-profile') {
            this._showStatusMessage();
            if (this._timeouts.loadingProfile)
                GLib.source_remove(this._timeouts.loadingProfile);
            this._timeouts.loadingProfile = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                100, () => {
                    this._activateVShell();
                    this._timeouts.loadingProfile = 0;
                    return GLib.SOURCE_REMOVE;
                });
        }
        if (this._timeouts.loadingProfile)
            return;

        if (key?.includes('profile-data')) {
            const index = key.replace('profile-data-', '');
            Main.notify(`${Me.metadata.name}`, `Profile ${index} has been updated`);
        }

        opt.WORKSPACE_MIN_SPACING = Main.overview._overview._controls._thumbnailsBox.get_theme_node().get_length('spacing');
        // update variables that cannot be processed within settings
        const dash = Main.overview.dash;
        if (Me.Util.dashIsDashToDock()) {
            opt.DASH_POSITION = dash._position;
            opt.DASH_TOP = opt.DASH_POSITION === 0;
            opt.DASH_RIGHT = opt.DASH_POSITION === 1;
            opt.DASH_BOTTOM = opt.DASH_POSITION === 2;
            opt.DASH_LEFT = opt.DASH_POSITION === 3;
            opt.DASH_VERTICAL = opt.DASH_LEFT || opt.DASH_RIGHT;
        }

        opt.DASH_VISIBLE = opt.DASH_VISIBLE && !Me.Util.getEnabledExtensions('dash-to-panel@jderose9.github.com').length;

        const monitorWidth = global.display.get_monitor_geometry(global.display.get_primary_monitor()).width;
        if (monitorWidth < 1600) {
            opt.APP_GRID_ICON_SIZE_DEFAULT = opt.APP_GRID_ACTIVE_PREVIEW && !opt.APP_GRID_USAGE ? 128 : 64;
            opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT = 64;
        }

        Workspace.WINDOW_PREVIEW_MAXIMUM_SCALE = opt.OVERVIEW_MODE === 1 ? 0.1 : 0.95;

        // adjust search entry style for OM2
        if (opt.OVERVIEW_MODE2)
            Main.overview.searchEntry.add_style_class_name('search-entry-om2');
        else
            Main.overview.searchEntry.remove_style_class_name('search-entry-om2');

        Main.overview.searchEntry.visible = opt.SHOW_SEARCH_ENTRY;
        Main.overview.searchEntry.opacity = 255;
        St.Settings.get().slow_down_factor = opt.ANIMATION_TIME_FACTOR;
        Search.MAX_LIST_SEARCH_RESULTS_ROWS = opt.SEARCH_MAX_ROWS;

        opt.START_Y_OFFSET = (opt.get('panelModule') && opt.PANEL_OVERVIEW_ONLY && opt.PANEL_POSITION_TOP) ||
            // better to add unnecessary space than to have a panel overlapping other objects
            Me.Util.getEnabledExtensions('hidetopbar').length
            ? Main.panel.height
            : 0;

        // Options for workspace switcher, apply custom function only if needed
        if (opt.WS_WRAPAROUND || opt.WS_IGNORE_LAST)
            Meta.Workspace.prototype.get_neighbor = this._getNeighbor;
        else
            Meta.Workspace.prototype.get_neighbor = this._originalGetNeighbor;

        if (settings)
            this._applySettings(key);
    }

    _applySettings(key) {
        if (key?.endsWith('-module')) {
            for (let module of Me.moduleList) {
                if (opt.options[module] && key === opt.options[module][1]) {
                    if (key === 'app-display-module')
                        this._showStatusMessage();
                    Me.Modules[module].update();
                    break;
                }
            }
            return;
        }

        Main.overview._overview.controls._setBackground();
        this._switchPageShortcuts();

        if (key?.includes('panel'))
            Me.Modules.panelModule.update();

        if (key?.includes('dash') || key?.includes('icon') || key?.includes('dot-style'))
            Me.Modules.dashModule.update();

        if (key?.includes('hot-corner') || key?.includes('dash'))
            Me.Modules.layoutModule.update();

        switch (key) {
        case 'ws-thumbnails-position':
            this._updateOverrides();
            break;
        case 'workspace-switcher-animation':
            Me.Modules.workspaceAnimationModule.update();
            break;
        case 'search-width-scale':
            Me.Modules.searchModule.update();
            break;
        case 'favorites-notify':
            Me.Modules.appFavoritesModule.update();
            break;
        case 'window-attention-mode':
            Me.Modules.windowAttentionHandlerModule.update();
            break;
        case 'show-ws-preview-bg':
            Me.Modules.panelModule.update();
            break;
        case 'notification-position':
            Me.Modules.messageTrayModule.update();
            break;
        case 'osd-position':
            Me.Modules.osdWindowModule.update();
            break;
        case 'overlay-key':
            Me.Modules.overlayKeyModule.update();
            break;
        case 'always-activate-selected-window':
            Me.Modules.windowPreviewModule.update();
            break;
        }

        if (key?.includes('app-grid') ||
            key?.includes('app-folder') ||
            key?.includes('dot-style') ||
            key === 'show-search-entry' ||
            key === 'ws-thumbnail-scale' ||
            key === 'ws-thumbnail-scale-appgrid') {
            this._showStatusMessage();
            Me.Modules.appDisplayModule.update();
        }
    }

    _switchPageShortcuts() {
        //                                          ignore screen lock
        if (!opt.get('enablePageShortcuts') || this._sessionLockActive)
            return;

        const vertical = global.workspaceManager.layout_rows === -1;
        const schema = 'org.gnome.desktop.wm.keybindings';
        const settings = new Gio.Settings({ schema_id: schema });

        const keyLeft = 'switch-to-workspace-left';
        const keyRight = 'switch-to-workspace-right';
        const keyUp = 'switch-to-workspace-up';
        const keyDown = 'switch-to-workspace-down';

        const keyMoveLeft = 'move-to-workspace-left';
        const keyMoveRight = 'move-to-workspace-right';
        const keyMoveUp = 'move-to-workspace-up';
        const keyMoveDown = 'move-to-workspace-down';

        const switchPrevSc = '<Super>Page_Up';
        const switchNextSc = '<Super>Page_Down';
        const movePrevSc = '<Super><Shift>Page_Up';
        const moveNextSc = '<Super><Shift>Page_Down';

        let switchLeft = settings.get_strv(keyLeft);
        let switchRight = settings.get_strv(keyRight);
        let switchUp = settings.get_strv(keyUp);
        let switchDown = settings.get_strv(keyDown);

        let moveLeft = settings.get_strv(keyMoveLeft);
        let moveRight = settings.get_strv(keyMoveRight);
        let moveUp = settings.get_strv(keyMoveUp);
        let moveDown = settings.get_strv(keyMoveDown);

        if (vertical) {
            if (switchLeft.includes(switchPrevSc))
                switchLeft.splice(switchLeft.indexOf(switchPrevSc), 1);
            if (switchRight.includes(switchNextSc))
                switchRight.splice(switchRight.indexOf(switchNextSc), 1);
            if (moveLeft.includes(movePrevSc))
                moveLeft.splice(moveLeft.indexOf(movePrevSc), 1);
            if (moveRight.includes(moveNextSc))
                moveRight.splice(moveRight.indexOf(moveNextSc), 1);

            if (!switchUp.includes(switchPrevSc))
                switchUp.push(switchPrevSc);
            if (!switchDown.includes(switchNextSc))
                switchDown.push(switchNextSc);
            if (!moveUp.includes(movePrevSc))
                moveUp.push(movePrevSc);
            if (!moveDown.includes(moveNextSc))
                moveDown.push(moveNextSc);
        } else {
            if (!switchLeft.includes(switchPrevSc))
                switchLeft.push(switchPrevSc);
            if (!switchRight.includes(switchNextSc))
                switchRight.push(switchNextSc);
            if (!moveLeft.includes(movePrevSc))
                moveLeft.push(movePrevSc);
            if (!moveRight.includes(moveNextSc))
                moveRight.push(moveNextSc);

            if (switchUp.includes(switchPrevSc))
                switchUp.splice(switchUp.indexOf(switchPrevSc), 1);
            if (switchDown.includes(switchNextSc))
                switchDown.splice(switchDown.indexOf(switchNextSc), 1);
            if (moveUp.includes(movePrevSc))
                moveUp.splice(moveUp.indexOf(movePrevSc), 1);
            if (moveDown.includes(moveNextSc))
                moveDown.splice(moveDown.indexOf(moveNextSc), 1);
        }

        settings.set_strv(keyLeft, switchLeft);
        settings.set_strv(keyRight, switchRight);
        settings.set_strv(keyUp, switchUp);
        settings.set_strv(keyDown, switchDown);

        settings.set_strv(keyMoveLeft, moveLeft);
        settings.set_strv(keyMoveRight, moveRight);
        settings.set_strv(keyMoveUp, moveUp);
        settings.set_strv(keyMoveDown, moveDown);
    }

    // Status dialog that appears during updating V-Shell configuration and blocks inputs
    _showStatusMessage(show = true) {
        if ((show && Me._resetInProgress) || Main.layoutManager._startingUp || !Main.overview._overview.controls._appDisplay._sortOrderedItemsAlphabetically)
            return;

        if (Me._vShellMessageTimeoutId) {
            GLib.source_remove(Me._vShellMessageTimeoutId);
            Me._vShellMessageTimeoutId = 0;
        }

        if (Me._vShellStatusMessage && !show) {
            Me._vShellStatusMessage.close();
            Me._vShellStatusMessage.destroy();
            Me._vShellStatusMessage = null;
        }

        if (!show)
            return;

        if (!Me._vShellStatusMessage) {
            const sm = new Main.RestartMessage(_('Updating V-Shell...'));
            sm.set_style('background-color: rgba(0,0,0,0.3);');
            sm.open();
            Me._vShellStatusMessage = sm;
        }

        // just for case the message wasn't removed from appDisplay after App Grid realization
        Me._vShellMessageTimeoutId = GLib.timeout_add_seconds(
            GLib.PRIORITY_DEFAULT,
            5,
            () => {
                if (Me._vShellStatusMessage) {
                    Me._vShellStatusMessage.close();
                    Me._vShellStatusMessage.destroy();
                    Me._vShellStatusMessage = null;
                    Me._resetInProgress = false;
                }

                Me._vShellMessageTimeoutId = 0;
                return GLib.SOURCE_REMOVE;
            }
        );
    }

    _getNeighbor(direction) {
        // workspace matrix is supported
        const activeIndex = this.index();
        const ignoreLast = opt.WS_IGNORE_LAST && !Main.overview._shown ? 1 : 0;
        const wraparound = opt.WS_WRAPAROUND;
        const nWorkspaces = global.workspace_manager.n_workspaces;
        const lastIndex = nWorkspaces - 1 - ignoreLast;
        const rows = global.workspace_manager.layout_rows > -1 ? global.workspace_manager.layout_rows : nWorkspaces;
        const columns = global.workspace_manager.layout_columns > -1 ? global.workspace_manager.layout_columns : nWorkspaces;

        let index = activeIndex;
        let neighborExists;

        if (direction === Meta.MotionDirection.LEFT) {
            index -= 1;
            const currentRow = Math.floor(activeIndex / columns);
            const indexRow = Math.floor(index / columns);
            neighborExists = index > -1 && indexRow === currentRow;
            if (wraparound && !neighborExists) {
                index = currentRow * columns + columns - 1;
                const maxIndexOnLastRow = lastIndex % columns;
                index = index < (lastIndex - ignoreLast) ? index : currentRow * columns + maxIndexOnLastRow;
            }
        } else if (direction === Meta.MotionDirection.RIGHT) {
            index += 1;
            const currentRow = Math.floor(activeIndex / columns);
            const indexRow = Math.floor(index / columns);
            neighborExists = index <= lastIndex && indexRow === currentRow;
            if (wraparound && !neighborExists)
                index = currentRow * columns;
        } else if (direction === Meta.MotionDirection.UP) {
            index -= columns;
            neighborExists = index > -1;
            if (wraparound && !neighborExists) {
                index = rows * columns + index;
                index = index < nWorkspaces - ignoreLast ? index : index - columns;
            }
        } else if (direction === Meta.MotionDirection.DOWN) {
            index += columns;
            neighborExists = index <= lastIndex;
            if (wraparound && !neighborExists)
                index %= columns;
        }

        return global.workspace_manager.get_workspace_by_index(neighborExists || wraparound ? index : activeIndex);
    }
}
