/*
 * Zorin Menu: The official applications menu for Zorin OS.
 *
 * Copyright (C) 2016-2018 Zorin OS Technologies Ltd.
 *
 * 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/>.
 */

// Import Libraries
const Lang = imports.lang;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Signals = imports.signals;
const AccountsService = imports.gi.AccountsService;
const Gio = imports.gi.Gio;
const Util = imports.misc.util;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']);
const _ = Gettext.gettext;
const SecondaryMenu = Me.imports.secondaryMenu;
const DND = imports.ui.dnd;
const AppDisplay = imports.ui.appDisplay;
const Mainloop = imports.mainloop;

const APPLICATION_ICON_SIZE = 32;
const ALL_ANIMATIONS = ~0;

function setIconAsync(icon, gioFile, fallback_icon_name) {
    gioFile.load_contents_async(null, function(source, result) {
        try {
            let bytes = source.load_contents_finish(result)[1];
            icon.gicon = Gio.BytesIcon.new(bytes);
        } catch(err) {
            icon.icon_name = fallback_icon_name;
        }
    });
}

// Vertical Separator
var VerticalSeparator = new Lang.Class({
    Name: 'VerticalSeparator',

    _init: function(session, accessible_name, icon_name) {
       this.actor = new St.DrawingArea({
            style_class: 'calendar-vertical-separator',
            pseudo_class: 'highlighted'
        });
        this.actor.connect('repaint', Lang.bind(this, this._repaint));
    },

    // Repaint vertical separator
    _repaint: function(area) {
        let cr = area.get_context();
        let themeNode = area.get_theme_node();
        let [width, height] = area.get_surface_size();
        let stippleColor = themeNode.get_color('-stipple-color');
        let stippleWidth = themeNode.get_length('-stipple-width');
        let x = Math.floor(width/2) + 0.5;
        cr.moveTo(x, 0);
        cr.lineTo(x, height);
        Clutter.cairo_set_source_color(cr, stippleColor);
        cr.setDash([1, 3], 1);
        cr.setLineWidth(stippleWidth);
        cr.stroke();
    }
});

// Removing the default behaviour which selects a hovered item if the space key is pressed.
// This avoids issues when searching for an app with a space character in its name.
var BaseMenuItem = new Lang.Class({
    Name: 'BaseMenuItem',
    Extends: PopupMenu.PopupBaseMenuItem,

    _onKeyPressEvent: function (actor, event) {
        let symbol = event.get_key_symbol();
        if (symbol == Clutter.KEY_Return ||
            symbol == Clutter.KEY_KP_Enter) {
            this.activate(event);
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    }
});

// Menu item to launch GNOME activities overview
var ActivitiesMenuItem = new Lang.Class({
    Name: 'ActivitiesMenuItem',
    Extends: BaseMenuItem,

    // Initialize the menu item
    _init: function() {
	    this.parent();
        this._icon = new St.Icon({
            icon_name: 'view-fullscreen-symbolic',
            style_class: 'popup-menu-icon',
            icon_size: 16
        });
        this.actor.add_child(this._icon);
        let label = new St.Label({
            text: _("Activities Overview"),
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(label, { expand: true });
    },

    // Activate the menu item (Open activities overview)
    activate: function(event) {
        this.emit('activated');
        Main.overview.toggle();
	    this.parent(event);
    },
});

// A base class for session buttons.
var SessionButton = new Lang.Class({
    Name: 'SessionButton',
    Abstract: true,

    _init: function(session, accessible_name, icon_name) {
        this._session = session;
        this.actor = new St.Button({
            reactive: true,
            can_focus: true,
            track_hover: true,
            accessible_name: accessible_name,
            style_class: 'system-menu-action'
        });
        this.actor.set_child( new St.Icon({ icon_name: icon_name }) );
        this.actor.connect('clicked', Lang.bind(this, this._onClick));
        this.actor.connect('key-press-event', this._onKeyPressEvent.bind(this));
    },

    _onKeyPressEvent: function (actor, event) {
        let symbol = event.get_key_symbol();
        if (symbol == Clutter.KEY_Return ||
            symbol == Clutter.KEY_KP_Enter) {
            this.activate();
            return Clutter.EVENT_STOP;
        } else if (symbol == Clutter.KEY_space ||
            symbol == Clutter.KEY_KP_Space) {
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    },

    _onClick: function() {
        this.emit('activated');
        this.activate();
    },

    activate: function() {
        // Button specific action
    }
});
Signals.addSignalMethods(SessionButton.prototype);

// Power Button
var PowerButton = new Lang.Class({
    Name: 'PowerButton',
    Extends: SessionButton,

    // Initialize the button
    _init: function(session) {
        this.parent(session, _("Power Off"), 'system-shutdown-symbolic');
    },

    // Activate the button (Shutdown)
    activate: function() {
        this._session.ShutdownRemote(0);
    }
});

// Logout Button
var LogoutButton = new Lang.Class({
    Name: 'LogoutButton',
    Extends: SessionButton,

    // Initialize the button
    _init: function(session) {
        this.parent(session, _("Log Out"), 'application-exit-symbolic');
    },

    // Activate the button (Logout)
    activate: function() {
        this._session.LogoutRemote(0);
    }
});

// Lock Screen Button
var LockButton = new Lang.Class({
    Name: 'LockButton',
    Extends: SessionButton,

    // Initialize the button
    _init: function(session) {
        this.parent(session, _("Lock"), 'changes-prevent-symbolic');
    },

    // Activate the button (Lock the screen)
    activate: function() {
        Main.screenShield.lock(true);
    }
});

// Menu item to go back to category view
var BackMenuItem = new Lang.Class({
    Name: 'BackMenuItem',
    Extends: BaseMenuItem,

    // Initialize the button
    _init: function() {
        this.parent();
        this._icon = new St.Icon({
            icon_name: 'go-previous-symbolic',
            style_class: 'popup-menu-icon',
            icon_size: APPLICATION_ICON_SIZE
        });
        this.actor.add_child(this._icon);
        let backLabel = new St.Label({
            text: _("Back"),
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(backLabel, { expand: true });
    },

    // Activate the button (go back to category view)
    activate: function(event) {
        this.emit('activated');
    },

    // Show Button
    show: function() {
        this.actor.show();
    },

    // Hide Button
    hide: function() {
        this.actor.hide();
    }
});

// Menu shortcut item class
var ShortcutMenuItem = new Lang.Class({
    Name: 'ShortcutMenuItem',
    Extends: BaseMenuItem,

    // Initialize the menu item
    _init: function(name, icon, command) {
        this.parent();
        this._command = command;
        this._icon = new St.Icon({
            icon_name: icon,
            style_class: 'popup-menu-icon',
            icon_size: 16
        });
        this.actor.add_child(this._icon);
        let label = new St.Label({
            text: name, y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(label, { expand: true });
    },

    // Activate the menu item (Launch the shortcut)
    activate: function(event) {
        this.emit('activated');
        Util.spawnCommandLine(this._command);
    }
});

// Menu item which displays the current user
var UserMenuItem = new Lang.Class({
    Name: 'UserMenuItem',
    Extends: BaseMenuItem,

    // Initialize the menu item
    _init: function() {
        this.parent();
        let username = GLib.get_user_name();
        this._user = AccountsService.UserManager.get_default().get_user(username);
        this._userIcon = new St.Icon({
            style_class: 'popup-menu-icon',
            icon_size: APPLICATION_ICON_SIZE
        });
        this.actor.add_child(this._userIcon);
        this._userLabel = new St.Label({
            text: username,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(this._userLabel, { expand: true });
        this._userLoadedId = this._user.connect('notify::is_loaded', Lang.bind(this, this._onUserChanged));
        this._userChangedId = this._user.connect('changed', Lang.bind(this, this._onUserChanged));
        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
        this._onUserChanged();
    },

    // Activate the menu item (Open user account settings)
    activate: function(event) {
        this.emit('activated');
        Util.spawnCommandLine("gnome-control-center user-accounts");
    },

    // Handle changes to user information (redisplay new info)
    _onUserChanged: function() {
        if (this._user.is_loaded) {
            this._userLabel.set_text (this._user.get_real_name());
            if (this._userIcon) {
                let iconFileName = this._user.get_icon_file();
                let iconFile = Gio.file_new_for_path(iconFileName);
                setIconAsync(this._userIcon, iconFile, 'avatar-default-symbolic');
            }
        }
    },

    // Destroy the menu item
    _onDestroy: function() {
        if (this._userLoadedId != 0) {
            this._user.disconnect(this._userLoadedId);
            this._userLoadedId = 0;
        }
        if (this._userChangedId != 0) {
            this._user.disconnect(this._userChangedId);
            this._userChangedId = 0;
        }
    }
});

// Menu application item class
var AppMenuItem = new Lang.Class({
    Name: 'AppMenuItem',
    Extends: BaseMenuItem,

    // Initialize menu item
    _init: function(app) {
        this.parent();
        this.app = app;
        this._iconBin = new St.Bin();
        this.actor.add_child(this._iconBin);

        let appLabel = new St.Label({
            text: app.get_name(),
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(appLabel, { expand: true });
        this.actor.label_actor = appLabel;

        let textureCache = St.TextureCache.get_default();
        let iconThemeChangedId = textureCache.connect('icon-theme-changed',
                                                      Lang.bind(this, this._updateIcon));
        this.actor.connect('destroy', Lang.bind(this, function() {
                textureCache.disconnect(iconThemeChangedId);
        }));
        this.actor.connect('popup-menu', Lang.bind(this, this._onKeyboardPopupMenu));
        this._updateIcon();
        this._menu = null;
        this._menuManager = new PopupMenu.PopupMenuManager(this);
        this._menuTimeoutId = 0;

        this._draggable = DND.makeDraggable(this.actor);
        this.isDraggableApp = true;
        this._draggable.connect('drag-begin', Lang.bind(this, function() {
            this._removeMenuTimeout();
            Main.overview.beginItemDrag(this);
        }));
        this._draggable.connect('drag-cancelled', Lang.bind(this, function() {
            Main.overview.cancelledItemDrag(this);
        }));
        this._draggable.connect('drag-end', Lang.bind(this, function() {
            Main.overview.endItemDrag(this);
        }));
    },

    get_app_id: function() {
        return this.app.get_id();
    },

    getDragActor: function() {
       return this.app.create_icon_texture(APPLICATION_ICON_SIZE);
    },

    // Returns the original actor that should align with the actor
    // we show as the item is being dragged.
    getDragActorSource: function() {
        return this.actor;
    },

    // Activate menu item (Launch application)
    activate: function(event) {
        this.app.open_new_window(-1);
        this.emit('activated');
        this.parent(event);
    },

    // Set button as active, scroll to the button
    setActive: function(active, params) {
        if (active && !this.actor.hover) {
            this.emit('scroll', this);
        }
        this.parent(active, params);
    },

    // Update the app icon in the menu
    _updateIcon: function() {
        this._iconBin.set_child(this.app.create_icon_texture(APPLICATION_ICON_SIZE));
    },

    _removeMenuTimeout: function() {
        if (this._menuTimeoutId > 0) {
            Mainloop.source_remove(this._menuTimeoutId);
            this._menuTimeoutId = 0;
        }
    },

    _setPopupTimeout: function() {
        this._removeMenuTimeout();
        this._menuTimeoutId = Mainloop.timeout_add(AppDisplay.MENU_POPUP_TIMEOUT,
            Lang.bind(this, function() {
                this._menuTimeoutId = 0;
                this.popupMenu();
                return GLib.SOURCE_REMOVE;
            }));
        GLib.Source.set_name_by_id(this._menuTimeoutId, '[gnome-shell] this.popupMenu');
    },

    _onLeaveEvent: function(actor, event) {
        this._removeMenuTimeout();
    },

    popupMenu: function() {
        this._removeMenuTimeout();

        if (this._draggable)
            this._draggable.fakeRelease();

        if (!this._menu) {
            this._menu = new SecondaryMenu.AppItemMenu(this);
            this._menu.connect('activate-window', Lang.bind(this, function (menu, window) {
                this.activateWindow(window);
            }));
            this._menu.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) {
                if (!isPoppedUp)
                    this._onMenuPoppedDown();
            }));
            let id = Main.overview.connect('hiding', Lang.bind(this, function () { this._menu.close(); }));
            this.actor.connect('destroy', function() {
                Main.overview.disconnect(id);
            });

            this._menuManager.addMenu(this._menu);
        }

        this.emit('menu-state-changed', true);

        this.actor.set_hover(true);
        this._menu.popup();
        this._menuManager.ignoreRelease();

        return false;
    },

    activateWindow: function(metaWindow) {
        if (metaWindow) {
            Main.activateWindow(metaWindow);
        } else {
            Main.overview.hide();
        }
        this.emit('activated');
    },

    animateLaunch: function() {
        this.emit('activated');
    },

    _onMenuPoppedDown: function() {
        this.actor.sync_hover();
        this.emit('menu-state-changed', false);
    },

    _onKeyboardPopupMenu: function() {
        this.popupMenu();
        this._menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
    },

    _onButtonPressEvent: function(actor, event) {
        this.actor.add_style_pseudo_class ('active');
        let button = event.get_button();
        if (button == 1) {
            this._setPopupTimeout();
        } else if (button == 3) {
            this.popupMenu();
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    },

    _onButtonReleaseEvent: function (actor, event) {
        this._removeMenuTimeout();
        this.actor.remove_style_pseudo_class ('active');
        let button = event.get_button();
        if (button != 3) {
            this.activate(event);
        }
        return Clutter.EVENT_STOP;
    },

    _onTouchEvent: function (actor, event) {
        if (event.type() == Clutter.EventType.TOUCH_BEGIN)
            this._setPopupTimeout();

        return Clutter.EVENT_PROPAGATE;
    },

    _onDestroy: function() {
        this.parent();
        this._removeMenuTimeout();
    }
});

// Menu Category item class
var CategoryMenuItem = new Lang.Class({
    Name: 'CategoryMenuItem',
    Extends: BaseMenuItem,

    // Initialize menu item
    _init: function(category) {
        this.parent();
        this._category = category;
        let name;
        if (this._category) {
            name = this._category.get_name();
        } else {
            name = _("Favorites");
        }
        this._icon = new St.Icon({
            gicon: this._category.get_icon(),
            style_class: 'popup-menu-icon',
            icon_size: APPLICATION_ICON_SIZE
        });
        this.actor.add_child(this._icon);
        let categoryLabel = new St.Label({
            text: name,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(categoryLabel, { expand: true });
        this.actor.label_actor = categoryLabel;
    },

    // Activate menu item (Display applications in category)
    activate: function(event) {
        this.emit('selected', this._category.get_menu_id());
        this.parent(event);
    },

    // Set button as active, scroll to the button
    setActive: function(active, params) {
        if (active && !this.actor.hover) {
            this.emit('scroll', this);
        }
        this.parent(active, params);
    }
});

// Place Info class
var PlaceInfo = new Lang.Class({
    Name: 'PlaceInfo',

    // Initialize place info
    _init: function(file, name, icon) {
        this.file = file;
        this.name = name ? name : this._getFileName();
        this.icon = icon ? new Gio.ThemedIcon({ name: icon }) : this.getIcon();
    },

    // Launch place with appropriate application
    launch: function(timestamp) {
        let launchContext = global.create_app_launch_context(timestamp, -1);
        Gio.AppInfo.launch_default_for_uri(this.file.get_uri(), launchContext);
    },

    // Get Icon for place
    getIcon: function() {
        try {
            let info = this.file.query_info('standard::symbolic-icon', 0, null);
	    return info.get_symbolic_icon();
        } catch(e if e instanceof Gio.IOErrorEnum) {
            if (!this.file.is_native()) {
                return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
            } else {
                return new Gio.ThemedIcon({ name: 'folder-symbolic' });
            }
        }
    },

    // Get display name for place
    _getFileName: function() {
        try {
            let info = this.file.query_info('standard::display-name', 0, null);
            return info.get_display_name();
        } catch(e if e instanceof Gio.IOErrorEnum) {
            return this.file.get_basename();
        }
    },
});
Signals.addSignalMethods(PlaceInfo.prototype);

// Menu Place Shortcut item class
var PlaceMenuItem = new Lang.Class({
    Name: 'PlaceMenuItem',
    Extends: BaseMenuItem,

    // Initialize menu item
    _init: function(info) {
        this.parent();
        this._info = info;
        this._icon = new St.Icon({
            gicon: info.icon,
            icon_size: 16
        });
	    this.actor.add_child(this._icon);
        this._label = new St.Label({
            text: info.name,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        this.actor.add_child(this._label, { expand: true });
        this._changedId = this._info.connect('changed',
                                       Lang.bind(this, this._propertiesChanged));
    },

    // Destroy menu item
    destroy: function() {
        if (this._changedId) {
            this._info.disconnect(this._changedId);
            this._changedId = 0;
        }
        this.parent();
    },

    // Activate (launch) the shortcut
    activate: function(event) {
        this._info.launch(event.get_time());
        this.emit('activated');
        this.parent(event);
    },

    // Handle changes in place info (redisplay new info)
    _propertiesChanged: function(info) {
        this._icon.gicon = info.icon;
        this._label.text = info.name;
    },
});

// Search Entry class
var SearchEntry = new Lang.Class({
    Name: 'SearchEntry',

    _init: function() {
        this.actor = new St.BoxLayout({
            style_class: 'search-box search-box-padding'
        });
        this._entry = new St.Entry({
            name: 'search-entry',
            hint_text: _("Type to search…"),
            track_hover: true,
            can_focus: true
        });
        this._searchIcon = new St.Icon({
            style_class: 'search-entry-icon',
            icon_name: 'edit-find-symbolic',
            icon_size: 16
        });
        this._clearIcon = new St.Icon({
            style_class: 'search-entry-icon',
            icon_name: 'edit-clear-symbolic',
            icon_size: 16
        });
        this._entry.set_primary_icon(this._searchIcon);
        this.actor.add(this._entry, {
            expand: true,
            x_align: St.Align.START,
            y_align: St.Align.START
        });
        this._entry.connect('secondary-icon-clicked', Lang.bind(this, function() {
            this.clear();
        }));
        this._previousInput = '';
        this._previousPattern = '';

        this._text = this._entry.get_clutter_text();
        this._text.connect('text-changed', Lang.bind(this, this._onTextChanged));

        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
    },

    getText: function() {
        return this._entry.get_text();
    },

    setText: function(text) {
        this._entry.set_text(text);
    },

    has_key_focus: function() {
        return this._entry.contains(global.stage.get_key_focus());
    },

    // Grab the key focus
    grab_key_focus: function() {
        this._entry.grab_key_focus();
    },

    // Clear the search box
    clear: function() {
        this._entry.set_text('');
        this._previousInput = '';
        this._previousPattern = '';
        this.grab_key_focus();
        this.emit('cleared');
    },

    _setClearIcon: function() {
       this._entry.set_secondary_icon(this._clearIcon);
    },

    _unsetClearIcon: function() {
        this._entry.set_secondary_icon(null);
    },

    // Handle search text entry input changes
    _onTextChanged: function() {
        let entryText = this._entry.get_text();
        if (entryText != '') {
            this._setClearIcon();
            let pattern = entryText.replace(/^\s+/g, '').replace(/\s+$/g, '').toLowerCase();
            if (pattern != this._previousPattern && pattern.length > 0) {
                this.emit('search', pattern);
            }
            this._previousInput = entryText;
            this._previousPattern = pattern;
        } else {
            this._unsetClearIcon();
            this._previousPattern = '';
            if (this._previousInput != '') {
                this._previousInput = '';
                this.emit('cleared');
            }
        }
    },

    _onDestroy: function() {
        this.emit('destroy');
    },
    
    destroy: function() {
        this.actor.destroy();
    }
});
Signals.addSignalMethods(SearchEntry.prototype);

// Menu Button icon
const menu_icon = Me.path + '/zorin-icon-symbolic.svg';

// Menu Button padding variable
const MINIMUM_PADDING = 4;

// Base Menu Button Icon Sizes
const baseMenuButtonIconSizes = [ 16, 22, 24, 32, 48, 64, 96, 128 ];

// Menu Button Widget
var MenuButton = new Lang.Class({
    Name: 'MenuButton',

    _init: function(panelActor) {
        this._panelActor = panelActor;
        this.actor = new St.BoxLayout({
            style_class: 'panel-status-menu-box',
            pack_start: false
        });

        this._iconSize = 32;

        this._icon = new St.Icon({
            gicon: Gio.icon_new_for_string(menu_icon),
            icon_size: this._iconSize,
            style_class: 'popup-menu-icon'
        });
        this._adjustIconSize();
        this.actor.add_child(this._icon);
        this._notifyHeightId = this._panelActor.connect('notify::height', Lang.bind(this, function() {
            this._adjustIconSize();
        }));
        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
    },

    _adjustIconSize: function() {
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let iconSizes = baseMenuButtonIconSizes.map(function(s) {
            return s * scaleFactor;
        });

        let availSize = this._panelActor.get_height() - (MINIMUM_PADDING * 2);

        let newIconSize = baseMenuButtonIconSizes[0];
        for (let i = 0; i < iconSizes.length ; i++) {
            if (iconSizes[i] < availSize) {
                newIconSize = baseMenuButtonIconSizes[i];
            }
        }

        if (newIconSize == this._iconSize)
            return;

        let oldIconSize = this._iconSize;
        this._iconSize = newIconSize;
        this.emit('icon-size-changed');
        this._icon.set_icon_size(this._iconSize);
        /*let scale = oldIconSize / newIconSize;
        let [targetWidth, targetHeight] = this._icon.get_size();

        // Scale the icon's texture to the previous size and
        // tween to the new size
        this._icon.set_size(this._icon.width * scale,
                           this._icon.height * scale);

        Tweener.addTween(this._icon,
                         { width: targetWidth,
                           height: targetHeight,
                           time: 0.2,
                           transition: 'easeOutQuad',
                         });*/
    },

    _onDestroy: function() {
        this._panelActor.disconnect(this._notifyHeightId);
        this.emit('destroy');
    },

    destroy: function() {
        this.actor.destroy();
    }
});
Signals.addSignalMethods(MenuButton.prototype);
