/*
 * This file is part of the Dash-To-Panel extension for Gnome 3
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Credits:
 * This file is based on code from the Dash to Dock extension by micheleg
 * 
 * Some code was also adapted from the upstream Gnome Shell source code.
 */

import * as Intellihide from './intellihide.js';
import * as Utils from './utils.js';

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as WindowManager from 'resource:///org/gnome/shell/ui/windowManager.js';
import {WindowPreview} from 'resource:///org/gnome/shell/ui/windowPreview.js';
import {InjectionManager} from 'resource:///org/gnome/shell/extensions/extension.js';
import {SETTINGS} from './extension.js';

const GS_HOTKEYS_KEY = 'switch-to-application-';

// When the dash is shown, workspace window preview bottom labels go over it (default
// gnome-shell behavior), but when the extension hides the dash, leave some space
// so those labels don't go over a bottom panel
const LABEL_MARGIN = 60;

//timeout names
const T1 = 'swipeEndTimeout';
const T2 = 'numberOverlayTimeout';

export const Overview = class {

    constructor() {
        this._injectionManager = new InjectionManager();
        this._numHotkeys = 10;
    }

    enable (primaryPanel) {
        this._panel = primaryPanel;
        this.taskbar = primaryPanel.taskbar;

        this._injectionsHandler = new Utils.InjectionsHandler();
        this._signalsHandler = new Utils.GlobalSignalsHandler();
        this._timeoutsHandler = new Utils.TimeoutsHandler();

        this._optionalWorkspaceIsolation();
        this._optionalHotKeys();
        this._optionalNumberOverlay();
        this._optionalClickToExit();

        this.toggleDash();
        this._adaptAlloc();

        this._signalsHandler.add([
            SETTINGS,
            [
                'changed::stockgs-keep-dash',
                'changed::panel-sizes'
            ], 
            () => this.toggleDash()
        ]);
    }

    disable() {
        this._signalsHandler.destroy();
        this._injectionsHandler.destroy();
        this._timeoutsHandler.destroy();
        this._injectionManager.clear();

        this.toggleDash(true);

        // Remove key bindings
        this._disableHotKeys();
        this._disableExtraShortcut();
        this._disableClickToExit();
    }

    toggleDash(visible) {
        if (visible === undefined) {
            visible = SETTINGS.get_boolean('stockgs-keep-dash');
        }

        let visibilityFunc = visible ? 'show' : 'hide';
        let height = visible ? -1 : LABEL_MARGIN * Utils.getScaleFactor();
        let overviewControls = Main.overview._overview._controls;

        overviewControls.dash[visibilityFunc]();
        overviewControls.dash.set_height(height);
    }

    _adaptAlloc() {
        let overviewControls = Main.overview._overview._controls

        this._injectionManager.overrideMethod(Object.getPrototypeOf(overviewControls), 'vfunc_allocate', 
            (originalAllocate) => 
                (box) => {
                    let focusedPanel = this._panel.panelManager.focusedMonitorPanel
                    
                    if (focusedPanel) {
                        let position = focusedPanel.geom.position
                        let isBottom = position == St.Side.BOTTOM

                        if (focusedPanel.intellihide?.enabled) {
                            // Panel intellihide is enabled (struts aren't taken into account on overview allocation),
                            // dynamically modify the overview box to follow the reveal/hide animation
                            let { transitioning, finalState, progress } = overviewControls._stateAdjustment.getStateTransitionParams()
                            let size = focusedPanel.geom[focusedPanel.checkIfVertical() ? 'w' : 'h'] * 
                                    (transitioning ? Math.abs((finalState != 0 ? 0 : 1) - progress) : 1)

                            if (isBottom || position == St.Side.RIGHT)
                                box[focusedPanel.fixedCoord.c2] -= size
                            else
                                box[focusedPanel.fixedCoord.c1] += size
                        } else if (isBottom)
                            // The default overview allocation is very good and takes into account external 
                            // struts, everywhere but the bottom where the dash is usually fixed anyway.
                            // If there is a bottom panel under the dash location, give it some space here
                            box.y2 -= focusedPanel.geom.h
                    }
                    
                    originalAllocate.call(overviewControls, box)
                }
        );
    }

    /**
     * Isolate overview to open new windows for inactive apps
     */
    _optionalWorkspaceIsolation() {
        let label = 'optionalWorkspaceIsolation';
        
        let enable = () => {
            this._injectionsHandler.removeWithLabel(label);

            this._injectionsHandler.addWithLabel(label, [
                Shell.App.prototype,
                'activate',
                IsolatedOverview
            ]);

            this._signalsHandler.removeWithLabel(label);

            this._signalsHandler.addWithLabel(label, [
                global.window_manager,
                'switch-workspace',
                () => this._panel.panelManager.allPanels.forEach(p => p.taskbar.handleIsolatedWorkspaceSwitch())
            ]);
        }

        let disable = () => {
            this._signalsHandler.removeWithLabel(label);
            this._injectionsHandler.removeWithLabel(label);
        }

        function IsolatedOverview() {
            // These lines take care of Nautilus for icons on Desktop
            let activeWorkspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace();
            let windows = this.get_windows().filter(w => w.get_workspace().index() == activeWorkspace.index());

            if (windows.length > 0 && 
                (!(windows.length == 1 && windows[0].skip_taskbar) || 
                 this.is_on_workspace(activeWorkspace)))
                return Main.activateWindow(windows[0]);
            
            return this.open_new_window(-1);
        }

        this._signalsHandler.add([
            SETTINGS,
            'changed::isolate-workspaces',
            () => {
                this._panel.panelManager.allPanels.forEach(p => p.taskbar.resetAppIcons());

                if (SETTINGS.get_boolean('isolate-workspaces'))
                    enable();
                else
                    disable();
            }
        ]);

        if (SETTINGS.get_boolean('isolate-workspaces'))
            enable();
    }

    // Hotkeys
    _activateApp(appIndex, modifiers) {
        let seenApps = {};
        let apps = [];
        
        this.taskbar._getAppIcons().forEach(appIcon => {
            if (!seenApps[appIcon.app] || this.taskbar.allowSplitApps) {
                apps.push(appIcon);
            }

            seenApps[appIcon.app] = (seenApps[appIcon.app] || 0) + 1;
        });

        this._showOverlay();

        if (appIndex < apps.length) {
            let appIcon = apps[appIndex];
            let seenAppCount = seenApps[appIcon.app];
            let windowCount = appIcon.window || appIcon._hotkeysCycle ? seenAppCount : appIcon._nWindows;

            if (SETTINGS.get_boolean('shortcut-previews') && windowCount > 1 && 
                !(modifiers & ~(Clutter.ModifierType.MOD1_MASK | Clutter.ModifierType.SUPER_MASK))) { //ignore the alt (MOD1_MASK) and super key (SUPER_MASK)
                if (this._hotkeyPreviewCycleInfo && this._hotkeyPreviewCycleInfo.appIcon != appIcon) {
                    this._endHotkeyPreviewCycle();
                }
                
                if (!this._hotkeyPreviewCycleInfo) {
                    this._hotkeyPreviewCycleInfo = {
                        appIcon: appIcon,
                        currentWindow: appIcon.window,
                        keyFocusOutId: appIcon.connect('key-focus-out', () => appIcon.grab_key_focus()),
                        capturedEventId: global.stage.connect('captured-event', (actor, e) => {
                            if (e.type() == Clutter.EventType.KEY_RELEASE && e.get_key_symbol() == (Clutter.KEY_Super_L || Clutter.Super_L)) {
                                this._endHotkeyPreviewCycle(true);
                            }
        
                            return Clutter.EVENT_PROPAGATE;
                        })
                    };

                    appIcon._hotkeysCycle = appIcon.window;
                    appIcon.window = null;
                    appIcon._previewMenu.open(appIcon, true);
                    appIcon.grab_key_focus();
                }
                
                appIcon._previewMenu.focusNext();
            } else {
                // Activate with button = 1, i.e. same as left click
                let button = 1;
                this._endHotkeyPreviewCycle();
                appIcon.activate(button, modifiers, !this.taskbar.allowSplitApps);
            }
        }
    }

    _endHotkeyPreviewCycle(focusWindow) {
        if (this._hotkeyPreviewCycleInfo) {
            global.stage.disconnect(this._hotkeyPreviewCycleInfo.capturedEventId);
            this._hotkeyPreviewCycleInfo.appIcon.disconnect(this._hotkeyPreviewCycleInfo.keyFocusOutId);

            if (focusWindow) {
                this._hotkeyPreviewCycleInfo.appIcon._previewMenu.activateFocused();
            } else
                this._hotkeyPreviewCycleInfo.appIcon._previewMenu.close()

            this._hotkeyPreviewCycleInfo.appIcon.window = this._hotkeyPreviewCycleInfo.currentWindow;
            delete this._hotkeyPreviewCycleInfo.appIcon._hotkeysCycle;
            this._hotkeyPreviewCycleInfo = 0;
        }
    }

    _optionalHotKeys() {
        this._hotKeysEnabled = false;
        if (SETTINGS.get_boolean('hot-keys'))
            this._enableHotKeys();

        this._signalsHandler.add([
            SETTINGS,
            'changed::hot-keys',
            () => {
                if (SETTINGS.get_boolean('hot-keys'))
                    this._enableHotKeys();
                else
                    this._disableHotKeys();
            }
        ]);
    }

    _resetHotkeys() {
        this._disableHotKeys();
        this._enableHotKeys();
    }

    _enableHotKeys() {
        if (this._hotKeysEnabled)
            return;

        //3.32 introduced app hotkeys, disable them to prevent conflicts
        if (Main.wm._switchToApplication) {
            for (let i = 1; i < 10; ++i) {
                Utils.removeKeybinding(GS_HOTKEYS_KEY + i);
            }
        }

        // Setup keyboard bindings for taskbar elements
        let shortcutNumKeys = SETTINGS.get_string('shortcut-num-keys');
        let bothNumKeys = shortcutNumKeys == 'BOTH';
        let keys = [];
        let prefixModifiers = Clutter.ModifierType.SUPER_MASK

        if (SETTINGS.get_string('hotkey-prefix-text') == 'SuperAlt')
            prefixModifiers |= Clutter.ModifierType.MOD1_MASK
        
        if (bothNumKeys || shortcutNumKeys == 'NUM_ROW') {
            keys.push('app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-'); // Regular numbers
        }
        
        if (bothNumKeys || shortcutNumKeys == 'NUM_KEYPAD') {
            keys.push('app-hotkey-kp-', 'app-shift-hotkey-kp-', 'app-ctrl-hotkey-kp-'); // Key-pad numbers
        }

        keys.forEach( function(key) {
            let modifiers = prefixModifiers
            
            // for some reason, in gnome-shell >= 40 Clutter.get_current_event() is now empty
            // for keyboard events. Create here the modifiers that are needed in appicon.activate 
            modifiers |= (key.indexOf('-shift-') >= 0 ? Clutter.ModifierType.SHIFT_MASK : 0)
            modifiers |= (key.indexOf('-ctrl-') >= 0 ? Clutter.ModifierType.CONTROL_MASK : 0)

            for (let i = 0; i < this._numHotkeys; i++) {
                let appNum = i;

                Utils.addKeybinding(key + (i + 1), SETTINGS, () => this._activateApp(appNum, modifiers));
            }
        }, this);

        this._hotKeysEnabled = true;

        if (SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS')
            this.taskbar.toggleNumberOverlay(true);
    }

    _disableHotKeys() {
        if (!this._hotKeysEnabled)
            return;

        let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-',  // Regular numbers
                    'app-hotkey-kp-', 'app-shift-hotkey-kp-', 'app-ctrl-hotkey-kp-']; // Key-pad numbers
        keys.forEach( function(key) {
            for (let i = 0; i < this._numHotkeys; i++) {
                Utils.removeKeybinding(key + (i + 1));
            }
        }, this);
        
        if (Main.wm._switchToApplication) {
            let gsSettings = new Gio.Settings({ schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA });

            for (let i = 1; i < 10; ++i) {
                Utils.addKeybinding(GS_HOTKEYS_KEY + i, gsSettings, Main.wm._switchToApplication.bind(Main.wm));
            }
        }

        this._hotKeysEnabled = false;

        this.taskbar.toggleNumberOverlay(false);
    }

    _optionalNumberOverlay() {
        // Enable extra shortcut
        if (SETTINGS.get_boolean('hot-keys'))
            this._enableExtraShortcut();

        this._signalsHandler.add([
            SETTINGS,
            'changed::hot-keys',
            this._checkHotkeysOptions.bind(this)
        ], [
            SETTINGS,
            'changed::hotkeys-overlay-combo',
            () => {
                if (SETTINGS.get_boolean('hot-keys') && SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS')
                    this.taskbar.toggleNumberOverlay(true);
                else
                    this.taskbar.toggleNumberOverlay(false);
            }
        ], [
            SETTINGS,
            'changed::shortcut-num-keys',
            () =>  this._resetHotkeys()
        ]);
    }

    _checkHotkeysOptions() {
        if (SETTINGS.get_boolean('hot-keys'))
            this._enableExtraShortcut();
        else
            this._disableExtraShortcut();
    }

    _enableExtraShortcut() {
        Utils.addKeybinding('shortcut', SETTINGS, () => this._showOverlay(true));
    }

    _disableExtraShortcut() {
        Utils.removeKeybinding('shortcut');
    }

    _showOverlay(overlayFromShortcut) {
        //wait for intellihide timeout initialization
        if (!this._panel.intellihide) {
            return;
        }

        // Restart the counting if the shortcut is pressed again
        let hotkey_option = SETTINGS.get_string('hotkeys-overlay-combo');

        if (hotkey_option === 'NEVER')
            return;

        if (hotkey_option === 'TEMPORARILY' || overlayFromShortcut)
            this.taskbar.toggleNumberOverlay(true);

        this._panel.intellihide.revealAndHold(Intellihide.Hold.TEMPORARY);

        let timeout = SETTINGS.get_int('overlay-timeout');
        
        if (overlayFromShortcut) {
            timeout = SETTINGS.get_int('shortcut-timeout');
        }

        // Hide the overlay/dock after the timeout
        this._timeoutsHandler.add([T2, timeout, () => {
            if (hotkey_option != 'ALWAYS') {
                this.taskbar.toggleNumberOverlay(false);
            }
            
            this._panel.intellihide.release(Intellihide.Hold.TEMPORARY);
        }]);
    }

    _optionalClickToExit() {
        this._clickToExitEnabled = false;
        if (SETTINGS.get_boolean('overview-click-to-exit'))
            this._enableClickToExit();

        this._signalsHandler.add([
            SETTINGS,
            'changed::overview-click-to-exit',
            () => {
                if (SETTINGS.get_boolean('overview-click-to-exit'))
                    this._enableClickToExit();
                else
                    this._disableClickToExit();
            }
        ]);
    }

    _enableClickToExit() {
        if (this._clickToExitEnabled)
            return;

        this._signalsHandler.addWithLabel('click-to-exit', [
            Main.layoutManager.overviewGroup,
            'button-release-event',
            () => {
                let [x, y] = global.get_pointer();
                let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
                
                if (pickedActor) {
                    let parent = pickedActor.get_parent();

                    if (
                        (
                         pickedActor.has_style_class_name &&
                         pickedActor.has_style_class_name('apps-scroll-view') &&
                         !pickedActor.has_style_pseudo_class('first-child')
                        ) || (
                         parent?.has_style_class_name &&
                         parent.has_style_class_name('window-picker')
                        ) ||
                        Main.overview._overview._controls._searchEntryBin.contains(pickedActor) ||
                        pickedActor instanceof WindowPreview
                    )
                        return Clutter.EVENT_PROPAGATE
                } 

                Main.overview.toggle()
            }
        ]);

        this._clickToExitEnabled = true;
    }

    _disableClickToExit() {
        if (!this._clickToExitEnabled)
            return;
        
        this._signalsHandler.removeWithLabel('click-to-exit')

        this._clickToExitEnabled = false;
    }

    _onSwipeBegin() {
        this._swiping = true;
        return true;
    }

    _onSwipeEnd() {
        this._timeoutsHandler.add([
            T1,
            0, 
            () => this._swiping = false
        ]);
        return true;
    }

}
