/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

const Atk = imports.gi.Atk;
const GMenu = imports.gi.GMenu;
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 PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Signals = imports.signals;
const Pango = imports.gi.Pango;

const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;

const appSys = Shell.AppSystem.get_default();

const APPLICATION_ICON_SIZE = 32;
const HORIZ_FACTOR = 5;
const MENU_HEIGHT_OFFSET = 132;
const NAVIGATION_REGION_OVERSHOOT = 50;

const ActivitiesMenuItem = new Lang.Class({
    Name: 'ActivitiesMenuItem',
    Extends: PopupMenu.PopupBaseMenuItem,

    _init: function(button) {
	this.parent();
        this._button = button;
        this.actor.add_child(new St.Label({ text: _("Activities Overview") }));
    },

    activate: function(event) {
        this._button.menu.toggle();
        Main.overview.toggle();
	this.parent(event);
    },
});

const ApplicationMenuItem = new Lang.Class({
    Name: 'ApplicationMenuItem',
    Extends: PopupMenu.PopupBaseMenuItem,

    _init: function(button, app) {
	this.parent();
	this._app = app;
        this._button = button;

        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._updateIcon();
    },

    activate: function(event) {
	this._app.open_new_window(-1);
        this._button.selectCategory(null, null);
        this._button.menu.toggle();
	this.parent(event);
    },

    setActive: function(active, params) {
        if (active)
            this._button.scrollToButton(this);
        this.parent(active, params);
    },

    _updateIcon: function() {
        this._iconBin.set_child(this._app.create_icon_texture(APPLICATION_ICON_SIZE));
    }
});

const CategoryMenuItem = new Lang.Class({
    Name: 'CategoryMenuItem',
    Extends: PopupMenu.PopupBaseMenuItem,

    _init: function(button, category) {
	this.parent();
	this._category = category;
        this._button = button;

        this._oldX = -1;
        this._oldY = -1;

        let name;
        if (this._category)
            name = this._category.get_name();
        else
            name = _("Favorites");

        this.actor.add_child(new St.Label({ text: name }));
        this.actor.connect('motion-event', Lang.bind(this, this._onMotionEvent));
    },

    activate: function(event) {
        this._button.selectCategory(this._category, this);
        this._button.scrollToCatButton(this);
	this.parent(event);
    },

    _isNavigatingSubmenu: function([x, y]) {
        let [posX, posY] = this.actor.get_transformed_position();

        if (this._oldX == -1) {
            this._oldX = x;
            this._oldY = y;
            return true;
        }

        let deltaX = Math.abs(x - this._oldX);
        let deltaY = Math.abs(y - this._oldY);

        this._oldX = x;
        this._oldY = y;

        // If it lies outside the x-coordinates then it is definitely outside.
        if (posX > x || posX + this.actor.width < x)
            return false;

        // If it lies inside the menu item then it is definitely inside.
        if (posY <= y && posY + this.actor.height >= y)
            return true;

        // We want the keep-up triangle only if the movement is more
        // horizontal than vertical.
        if (deltaX * HORIZ_FACTOR < deltaY)
            return false;

        // Check whether the point lies inside triangle ABC, and a similar
        // triangle on the other side of the menu item.
        //
        //   +---------------------+
        //   | menu item           |
        // A +---------------------+ C
        //              P          |
        //                         B

        // Ensure that the point P always lies below line AC so that we can
        // only check for triangle ABC.
        if (posY > y) {
            let offset = posY - y;
            y = posY + this.actor.height + offset;
        }

        // Ensure that A is (0, 0).
        x -= posX;
        y -= posY + this.actor.height;

        // Check which side of line AB the point P lies on by taking the
        // cross-product of AB and AP. See:
        // http://stackoverflow.com/questions/3461453/determine-which-side-of-a-line-a-point-lies
        if (((this.actor.width * y) - (NAVIGATION_REGION_OVERSHOOT * x)) <= 0)
             return true;

        return false;
    },

    _onMotionEvent: function(actor, event) {
        if (!Clutter.get_pointer_grab()) {
            this._oldX = -1;
            this._oldY = -1;
            Clutter.grab_pointer(this.actor);
        }
        this.actor.hover = true;

        if (this._isNavigatingSubmenu(event.get_coords()))
            return true;

        this._oldX = -1;
        this._oldY = -1;
        this.actor.hover = false;
        Clutter.ungrab_pointer();

        let source = event.get_source();
        if (source instanceof St.Widget)
            source.sync_hover();

        return false;
    },

    setActive: function(active, params) {
        if (active) {
            this._button.selectCategory(this._category, this);
            this._button.scrollToCatButton(this);
        }
        this.parent(active, params);
    }
});

const ApplicationsMenu = new Lang.Class({
    Name: 'ApplicationsMenu',
    Extends: PopupMenu.PopupMenu,

    _init: function(sourceActor, arrowAlignment, arrowSide, button) {
        this.parent(sourceActor, arrowAlignment, arrowSide);
        this._button = button;
    },

    isEmpty: function() {
	return false;
    },

    open: function(animate) {
        this._button.hotCorner.setBarrierSize(0);
        if (this._button.hotCorner.actor) // fallback corner
            this._button.hotCorner.actor.hide();
        this.parent(animate);
    },

    close: function(animate) {
        let size = Main.layoutManager.panelBox.height;
        this._button.hotCorner.setBarrierSize(size);
        if (this._button.hotCorner.actor) // fallback corner
            this._button.hotCorner.actor.show();
        this.parent(animate);
    },

    toggle: function() {
        if (this.isOpen) {
            this._button.selectCategory(null, null);
        } else {
            if (Main.overview.visible)
                Main.overview.hide();
        }
        this.parent();
    }
});

const ApplicationsButton = new Lang.Class({
    Name: 'ApplicationsButton',
    Extends: PanelMenu.Button,

    _init: function() {
        this.parent(1.0, null, false);

        this.setMenu(new ApplicationsMenu(this.actor, 1.0, St.Side.TOP, this));
        Main.panel.menuManager.addMenu(this.menu);

        // At this moment applications menu is not keyboard navigable at
        // all (so not accessible), so it doesn't make sense to set as
        // role ATK_ROLE_MENU like other elements of the panel.
        this.actor.accessible_role = Atk.Role.LABEL;

        let hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });

        this._label = new St.Label({ text: _("Applications"),
                                     y_expand: true,
                                     y_align: Clutter.ActorAlign.CENTER });
        hbox.add_child(this._label);
        hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));

        this.actor.add_actor(hbox);
        this.actor.name = 'panelApplications';
        this.actor.label_actor = this._label;

        this.actor.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));

        this._showingId = Main.overview.connect('showing', Lang.bind(this, function() {
            this.actor.add_accessible_state (Atk.StateType.CHECKED);
        }));
        this._hidingId = Main.overview.connect('hiding', Lang.bind(this, function() {
            this.actor.remove_accessible_state (Atk.StateType.CHECKED);
        }));
        Main.layoutManager.connect('startup-complete',
                                   Lang.bind(this, this._setKeybinding));
        this._setKeybinding();

        this.reloadFlag = false;
        this._createLayout();
        this._display();
        this._installedChangedId = appSys.connect('installed-changed', Lang.bind(this, function() {
            if (this.menu.isOpen) {
                this._redisplay();
                this.mainBox.show();
            } else {
                this.reloadFlag = true;
            }
        }));

        // Since the hot corner uses stage coordinates, Clutter won't
        // queue relayouts for us when the panel moves. Queue a relayout
        // when that happens.
        this._panelBoxChangedId = Main.layoutManager.connect('panel-box-changed', Lang.bind(this, function() {
            container.queue_relayout();
        }));
    },

    get hotCorner() {
        return Main.layoutManager.hotCorners[Main.layoutManager.primaryIndex];
    },

    _createVertSeparator: function() {
        let separator = new St.DrawingArea({ style_class: 'calendar-vertical-separator',
                                             pseudo_class: 'highlighted' });
        separator.connect('repaint', Lang.bind(this, this._onVertSepRepaint));
        return separator;
    },

    _onDestroy: function() {
        Main.overview.disconnect(this._showingId);
        Main.overview.disconnect(this._hidingId);
        Main.layoutManager.disconnect(this._panelBoxChangedId);
        appSys.disconnect(this._installedChangedId);

        Main.wm.setCustomKeybindingHandler('panel-main-menu',
                                           Shell.ActionMode.NORMAL |
                                           Shell.ActionMode.OVERVIEW,
                                           Main.sessionMode.hasOverview ?
                                           Lang.bind(Main.overview, Main.overview.toggle) :
                                           null);
    },

    _onCapturedEvent: function(actor, event) {
        if (event.type() == Clutter.EventType.BUTTON_PRESS) {
            if (!Main.overview.shouldToggleByCornerOrButton())
                return true;
        }
        return false;
    },

    _onMenuKeyPress: function(actor, event) {
        let symbol = event.get_key_symbol();
        if (symbol == Clutter.KEY_Left || symbol == Clutter.KEY_Right) {
            let direction = symbol == Clutter.KEY_Left ? Gtk.DirectionType.LEFT
                                                       : Gtk.DirectionType.RIGHT;
            if (this.menu.actor.navigate_focus(global.stage.key_focus, direction, false))
                return true;
        }
        return this.parent(actor, event);
    },

    _onVertSepRepaint: 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); // Hard-code for now
        cr.setLineWidth(stippleWidth);
        cr.stroke();
    },

    _onOpenStateChanged: function(menu, open) {
       if (open) {
           if (this.reloadFlag) {
               this._redisplay();
               this.reloadFlag = false;
           }
           this.mainBox.show();
       }
       this.parent(menu, open);
    },

    _setKeybinding: function() {
        Main.wm.setCustomKeybindingHandler('panel-main-menu',
                                           Shell.ActionMode.NORMAL |
                                           Shell.ActionMode.OVERVIEW,
                                           Lang.bind(this, function() {
                                               this.menu.toggle();
                                           }));
    },

    _redisplay: function() {
        this.applicationsBox.destroy_all_children();
        this.categoriesBox.destroy_all_children();
        this._display();
    },

    _loadCategory: function(categoryId, dir) {
        let iter = dir.iter();
        let nextType;
        while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
            if (nextType == GMenu.TreeItemType.ENTRY) {
                let entry = iter.get_entry();
                let id;
                try {
                    id = entry.get_desktop_file_id(); // catch non-UTF8 filenames
                } catch(e) {
                    continue;
                }
                let app = appSys.lookup_app(id);
                if (app.get_app_info().should_show())
                    this.applicationsByCategory[categoryId].push(app);
            } else if (nextType == GMenu.TreeItemType.DIRECTORY) {
                let subdir = iter.get_directory();
                if (!subdir.get_is_nodisplay())
                    this._loadCategory(categoryId, subdir);
            }
        }
    },

    scrollToButton: function(button) {
        let appsScrollBoxAdj = this.applicationsScrollBox.get_vscroll_bar().get_adjustment();
        let appsScrollBoxAlloc = this.applicationsScrollBox.get_allocation_box();
        let currentScrollValue = appsScrollBoxAdj.get_value();
        let boxHeight = appsScrollBoxAlloc.y2 - appsScrollBoxAlloc.y1;
        let buttonAlloc = button.actor.get_allocation_box();
        let newScrollValue = currentScrollValue;
        if (currentScrollValue > buttonAlloc.y1 - 10)
            newScrollValue = buttonAlloc.y1 - 10;
        if (boxHeight + currentScrollValue < buttonAlloc.y2 + 10)
            newScrollValue = buttonAlloc.y2 - boxHeight + 10;
        if (newScrollValue != currentScrollValue)
            appsScrollBoxAdj.set_value(newScrollValue);
    },

    scrollToCatButton: function(button) {
        let catsScrollBoxAdj = this.categoriesScrollBox.get_vscroll_bar().get_adjustment();
        let catsScrollBoxAlloc = this.categoriesScrollBox.get_allocation_box();
        let currentScrollValue = catsScrollBoxAdj.get_value();
        let boxHeight = catsScrollBoxAlloc.y2 - catsScrollBoxAlloc.y1;
        let buttonAlloc = button.actor.get_allocation_box();
        let newScrollValue = currentScrollValue;
        if (currentScrollValue > buttonAlloc.y1 - 10)
            newScrollValue = buttonAlloc.y1 - 10;
        if (boxHeight + currentScrollValue < buttonAlloc.y2 + 10)
            newScrollValue = buttonAlloc.y2 - boxHeight + 10;
        if (newScrollValue != currentScrollValue)
            catsScrollBoxAdj.set_value(newScrollValue);
    },

    _createLayout: function() {
        let section = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(section);
        this.mainBox = new St.BoxLayout({ vertical: false });
        this.leftBox = new St.BoxLayout({ vertical: true });
        this.applicationsScrollBox = new St.ScrollView({ x_fill: true, y_fill: false,
                                                         y_align: St.Align.START,
                                                         style_class: 'apps-menu vfade' });
        this.applicationsScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
        let vscroll = this.applicationsScrollBox.get_vscroll_bar();
        vscroll.connect('scroll-start', Lang.bind(this, function() {
            this.menu.passEvents = true;
        }));
        vscroll.connect('scroll-stop', Lang.bind(this, function() {
            this.menu.passEvents = false;
        }));
        this.categoriesScrollBox = new St.ScrollView({ x_fill: true, y_fill: false,
                                                       y_align: St.Align.START,
                                                       style_class: 'vfade' });
        this.categoriesScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
        vscroll = this.categoriesScrollBox.get_vscroll_bar();
        vscroll.connect('scroll-start', Lang.bind(this, function() {
                              this.menu.passEvents = true;
                          }));
        vscroll.connect('scroll-stop', Lang.bind(this, function() {
            this.menu.passEvents = false;
        }));
        this.leftBox.add(this.categoriesScrollBox, { expand: true,
                                                     x_fill: true, y_fill: true,
                                                     y_align: St.Align.START });

        let activities = new ActivitiesMenuItem(this);
        this.leftBox.add(activities.actor, { expand: false,
                                             x_fill: true, y_fill: false,
                                             y_align: St.Align.START });

        this.applicationsBox = new St.BoxLayout({ vertical: true });
        this.applicationsScrollBox.add_actor(this.applicationsBox);
        this.categoriesBox = new St.BoxLayout({ vertical: true });
        this.categoriesScrollBox.add_actor(this.categoriesBox, { expand: true, x_fill: false });

        this.mainBox.add(this.leftBox);
        this.mainBox.add(this._createVertSeparator(), { expand: false, x_fill: false, y_fill: true});
        this.mainBox.add(this.applicationsScrollBox, { expand: true, x_fill: true, y_fill: true });
        section.actor.add_actor(this.mainBox);
    },

    _display: function() {
        this._applicationsButtons = new Array();
        this.mainBox.style=('width: 35em;');
        this.mainBox.hide();

        //Load categories
        this.applicationsByCategory = {};
        let tree = new GMenu.Tree({ menu_basename: 'applications.menu' });
        tree.load_sync();
        let root = tree.get_root_directory();
        let categoryMenuItem = new CategoryMenuItem(this, null);
        this.categoriesBox.add_actor(categoryMenuItem.actor);
        let iter = root.iter();
        let nextType;
        while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
            if (nextType == GMenu.TreeItemType.DIRECTORY) {
                let dir = iter.get_directory();
                if (!dir.get_is_nodisplay()) {
                    let categoryId = dir.get_menu_id();
                    this.applicationsByCategory[categoryId] = [];
                    this._loadCategory(categoryId, dir);
                    if (this.applicationsByCategory[categoryId].length > 0) {
                        let categoryMenuItem = new CategoryMenuItem(this, dir);
                        this.categoriesBox.add_actor(categoryMenuItem.actor);
                    }
                }
            }
        }

        //Load applications
        this._displayButtons(this._listApplications(null));

        let height = this.categoriesBox.height + MENU_HEIGHT_OFFSET + 'px';
        this.mainBox.style+=('height: ' + height);
    },

    _clearApplicationsBox: function(selectedActor) {
        let actors = this.applicationsBox.get_children();
        for (let i = 0; i < actors.length; i++) {
            let actor = actors[i];
            this.applicationsBox.remove_actor(actor);
        }
    },

    selectCategory: function(dir, categoryMenuItem) {
        if (categoryMenuItem)
            this._clearApplicationsBox(categoryMenuItem.actor);
        else
            this._clearApplicationsBox(null);

        if (dir)
            this._displayButtons(this._listApplications(dir.get_menu_id()));
        else
            this._displayButtons(this._listApplications(null));
    },

    _displayButtons: function(apps) {
         if (apps) {
            for (let i = 0; i < apps.length; i++) {
               let app = apps[i];
               if (!this._applicationsButtons[app]) {
                  let applicationMenuItem = new ApplicationMenuItem(this, app);
                  this._applicationsButtons[app] = applicationMenuItem;
               }
               if (!this._applicationsButtons[app].actor.get_parent())
                  this.applicationsBox.add_actor(this._applicationsButtons[app].actor);
            }
         }
    },

    _listApplications: function(category_menu_id) {
        let applist;

        if (category_menu_id) {
            applist = this.applicationsByCategory[category_menu_id];
            applist.sort(function(a,b) {
                return a.get_name().toLowerCase() > b.get_name().toLowerCase();
            });
        } else {
            applist = new Array();
            let favorites = global.settings.get_strv('favorite-apps');
            for (let i = 0; i < favorites.length; i++) {
                let app = appSys.lookup_app(favorites[i]);
                if (app)
                    applist.push(app);
            }
        }

        return applist;
    },

    destroy: function() {
        this.menu.actor.get_children().forEach(function(c) { c.destroy() });
        this.parent();
    }
});

let appsMenuButton;
let activitiesButton;

function enable() {
    activitiesButton = Main.panel.statusArea['activities'];
    activitiesButton.container.hide();
    appsMenuButton = new ApplicationsButton();
    Main.panel.addToStatusArea('apps-menu', appsMenuButton, 1, 'left');
}

function disable() {
    Main.panel.menuManager.removeMenu(appsMenuButton.menu);
    appsMenuButton.destroy();
    activitiesButton.container.show();
}

function init(metadata) {
    Convenience.initTranslations();
}
