/*
 * 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.
 */

const Me = imports.misc.extensionUtils.getCurrentExtension();
const Intellihide = Me.imports.intellihide;
const Utils = Me.imports.utils;

const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const Main = imports.ui.main;
const Shell = imports.gi.Shell;
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const Mainloop = imports.mainloop;
const IconGrid = imports.ui.iconGrid;
const ViewSelector = imports.ui.viewSelector;

const Meta = imports.gi.Meta;

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

//timeout names
const T1 = 'swipeEndTimeout';

var dtpOverview = Utils.defineClass({
    Name: 'DashToPanel.Overview',

    _init: function() {
        this._numHotkeys = 10;
        this._timeoutsHandler = new Utils.TimeoutsHandler();
    },

    enable : function(panel) {
        this._panel = panel;
        this.taskbar = panel.taskbar;

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

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

        this._signalsHandler.add([
            Me.settings,
            'changed::stockgs-keep-dash', 
            () => this._toggleDash()
        ]);
    },

    disable: function () {
        this._signalsHandler.destroy();
        this._injectionsHandler.destroy();
        
        this._toggleDash(true);

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

    _toggleDash: function(visible) {
        // To hide the dash, set its width to 1, so it's almost not taken into account by code
        // calculaing the reserved space in the overview. The reason to keep it at 1 is
        // to allow its visibility change to trigger an allocaion of the appGrid which
        // in turn is triggergin the appsIcon spring animation, required when no other
        // actors has this effect, i.e in horizontal mode and without the workspaceThumnails
        // 1 static workspace only)

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

        let visibilityFunc = visible ? 'show' : 'hide';
        let width = visible ? -1 : 1;
        let overviewControls = Main.overview._overview._controls || Main.overview._controls;

        overviewControls.dash.actor[visibilityFunc]();
        overviewControls.dash.actor.set_width(width);

        // This force the recalculation of the icon size
        overviewControls.dash._maxHeight = -1;
    },

    /**
     * Isolate overview to open new windows for inactive apps
     */
    _optionalWorkspaceIsolation: function() {
        let label = 'optionalWorkspaceIsolation';
        
        this._signalsHandler.add([
            Me.settings,
            'changed::isolate-workspaces',
            Lang.bind(this, function() {
                this._panel.panelManager.allPanels.forEach(p => p.taskbar.resetAppIcons());

                if (Me.settings.get_boolean('isolate-workspaces'))
                    Lang.bind(this, enable)();
                else
                    Lang.bind(this, disable)();
            })
        ]);

        if (Me.settings.get_boolean('isolate-workspaces'))
            Lang.bind(this, enable)();

        function 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())
            ]);
        }

        function 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);
        }
    },

    // Hotkeys
    _activateApp: function(appIndex) {
        let seenApps = {};
        let apps = [];
        
        this.taskbar._getAppIcons().forEach(function(appIcon) {
            if (!seenApps[appIcon.app]) {
                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 (Me.settings.get_boolean('shortcut-previews') && windowCount > 1 && 
                !(Clutter.get_current_event().get_state() & ~(Clutter.ModifierType.MOD1_MASK | Clutter.ModifierType.MOD4_MASK))) { //ignore the alt (MOD1_MASK) and super key (MOD4_MASK)
                if (this._hotkeyPreviewCycleInfo && this._hotkeyPreviewCycleInfo.appIcon != appIcon) {
                    this._endHotkeyPreviewCycle();
                }
                
                if (!this._hotkeyPreviewCycleInfo) {
                    this._hotkeyPreviewCycleInfo = {
                        appIcon: appIcon,
                        currentWindow: appIcon.window,
                        keyFocusOutId: appIcon.actor.connect('key-focus-out', () => appIcon.actor.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);
                    appIcon.actor.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, true);
            }
        }
    },

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

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

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

    _optionalHotKeys: function() {
        this._hotKeysEnabled = false;
        if (Me.settings.get_boolean('hot-keys'))
            this._enableHotKeys();

        this._signalsHandler.add([
            Me.settings,
            'changed::hot-keys',
            Lang.bind(this, function() {
                    if (Me.settings.get_boolean('hot-keys'))
                        Lang.bind(this, this._enableHotKeys)();
                    else
                        Lang.bind(this, this._disableHotKeys)();
            })
        ]);
    },

    _resetHotkeys: function() {
        this._disableHotKeys();
        this._enableHotKeys();
    },

    _enableHotKeys: function() {
        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 = Me.settings.get_string('shortcut-num-keys');
        let bothNumKeys = shortcutNumKeys == 'BOTH';
        let keys = [];
        
        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) {
            for (let i = 0; i < this._numHotkeys; i++) {
                let appNum = i;

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

        this._hotKeysEnabled = true;

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

    _disableHotKeys: function() {
        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: imports.ui.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: function() {
        // Enable extra shortcut
        if (Me.settings.get_boolean('hot-keys'))
            this._enableExtraShortcut();

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

    _checkHotkeysOptions: function() {
        if (Me.settings.get_boolean('hot-keys'))
            this._enableExtraShortcut();
        else
            this._disableExtraShortcut();
    },

    _enableExtraShortcut: function() {
        Utils.addKeybinding('shortcut', Me.settings, () => this._showOverlay(true));
    },

    _disableExtraShortcut: function() {
        Utils.removeKeybinding('shortcut');
    },

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

        // Restart the counting if the shortcut is pressed again
        if (this._numberOverlayTimeoutId) {
            Mainloop.source_remove(this._numberOverlayTimeoutId);
            this._numberOverlayTimeoutId = 0;
        }

        let hotkey_option = Me.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 = Me.settings.get_int('overlay-timeout');
        
        if (overlayFromShortcut) {
            timeout = Me.settings.get_int('shortcut-timeout');
        }

        // Hide the overlay/dock after the timeout
        this._numberOverlayTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, function() {
            this._numberOverlayTimeoutId = 0;
            
            if (hotkey_option != 'ALWAYS') {
                this.taskbar.toggleNumberOverlay(false);
            }
            
            this._panel.intellihide.release(Intellihide.Hold.TEMPORARY);
        }));
    },

    _optionalClickToExit: function() {
        this._clickToExitEnabled = false;
        if (Me.settings.get_boolean('overview-click-to-exit'))
            this._enableClickToExit();

        this._signalsHandler.add([
            Me.settings,
            'changed::overview-click-to-exit',
            Lang.bind(this, function() {
                    if (Me.settings.get_boolean('overview-click-to-exit'))
                        Lang.bind(this, this._enableClickToExit)();
                    else
                        Lang.bind(this, this._disableClickToExit)();
            })
        ]);
    },

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

        let views = Utils.getAppDisplayViews();
        this._oldOverviewReactive = Main.overview._overview.reactive

        Main.overview._overview.reactive = true;

        this._clickAction = new Clutter.ClickAction();
        this._clickAction.connect('clicked', () => {
            
            if (this._swiping)
                return Clutter.EVENT_PROPAGATE;
  
            let [x, y] = global.get_pointer();
            let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);

            let activePage = Main.overview.viewSelector.getActivePage();
            if (activePage == ViewSelector.ViewPage.APPS) {

                if(pickedActor != Main.overview._overview 
                    && (views.length > 1 && pickedActor != Main.overview.viewSelector.appDisplay._controls.get_parent())
                    && pickedActor != (views[0].view.actor || views[0].view)
                    && (views.length > 1 && pickedActor != views[1].view._scrollView)
                    && (views.length > 1 && pickedActor != views[1].view._grid)) {
                    return Clutter.EVENT_PROPAGATE;
                }

                if(Me.settings.get_boolean('animate-show-apps')) {
                    let view = Utils.find(views, v => v.view.actor.visible).view;
                    view.animate(IconGrid.AnimationDirection.OUT, Lang.bind(this, function() {
                        Main.overview.viewSelector._appsPage.hide();
                        Main.overview.hide();
                    }));
                } else {
                    Main.overview.hide();
                }
            } else if (activePage == ViewSelector.ViewPage.WINDOWS) {
                let overviewControls = Main.overview._overview._controls || Main.overview._controls;

                if(pickedActor == overviewControls._thumbnailsBox
                    || pickedActor == overviewControls.dash._container) {
                    return Clutter.EVENT_PROPAGATE;
                }

                if (pickedActor instanceof Meta.BackgroundActor) {
                    Utils.find(overviewControls._thumbnailsBox._thumbnails, t =>
                        pickedActor == t._bgManager.backgroundActor
                    ).activate();
                    return Clutter.EVENT_STOP;
                }

                Main.overview.toggle();
            } else {
                Main.overview.toggle();
            }
         });
         Main.overview._overview.add_action(this._clickAction);

        [Main.overview.viewSelector._workspacesDisplay].concat(views.map(v => v.view)).forEach(v => {
            if (v._swipeTracker) {
                this._signalsHandler.addWithLabel('clickToExit', [
                    v._swipeTracker,
                    'begin',
                    Lang.bind(this, this._onSwipeBegin)
                ],[
                    v._swipeTracker,
                    'end',
                    Lang.bind(this, this._onSwipeEnd)
                ]);
            } else if (v._panAction) {
                this._signalsHandler.addWithLabel('clickToExit', [
                    v._panAction,
                    'gesture-begin',
                    Lang.bind(this, this._onSwipeBegin)
                ],[
                    v._panAction,
                    'gesture-cancel',
                    Lang.bind(this, this._onSwipeEnd)
                ],[
                    v._panAction,
                    'gesture-end',
                    Lang.bind(this, this._onSwipeEnd)
                ]);
            }
        });

        this._clickToExitEnabled = true;
    },

    _disableClickToExit: function () {
        if (!this._clickToExitEnabled)
            return;
        
        Main.overview._overview.remove_action(this._clickAction);
        Main.overview._overview.reactive = this._oldOverviewReactive;

        this._signalsHandler.removeWithLabel('clickToExit');
    
        this._clickToExitEnabled = false;
    },

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

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