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

const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Lang = imports.lang;

const AppDisplay = imports.ui.appDisplay;
const DocDisplay = imports.ui.docDisplay;
const Places = imports.ui.places;
const GenericDisplay = imports.ui.genericDisplay;
const Button = imports.ui.button;
const Main = imports.ui.main;

const DEFAULT_PADDING = 4;
const DASH_SECTION_PADDING = 6;
const DASH_SECTION_SPACING = 12;
const DASH_CORNER_RADIUS = 5;
const DASH_SEARCH_BG_COLOR = new Clutter.Color();
DASH_SEARCH_BG_COLOR.from_pixel(0xffffffff);
const DASH_SECTION_COLOR = new Clutter.Color();
DASH_SECTION_COLOR.from_pixel(0x846c3dff);
const DASH_TEXT_COLOR = new Clutter.Color();
DASH_TEXT_COLOR.from_pixel(0xffffffff);

const PANE_BORDER_COLOR = new Clutter.Color();
PANE_BORDER_COLOR.from_pixel(0x213b5dfa);
const PANE_BORDER_WIDTH = 2;

const PANE_BACKGROUND_COLOR = new Clutter.Color();
PANE_BACKGROUND_COLOR.from_pixel(0x0d131ff4);


function Pane() {
    this._init();
}

Pane.prototype = {
    _init: function () {
        this._open = false;

        this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                   background_color: PANE_BACKGROUND_COLOR,
                                   border: PANE_BORDER_WIDTH,
                                   border_color: PANE_BORDER_COLOR,
                                   padding: DEFAULT_PADDING,
                                   reactive: true });
        this.actor.connect('button-press-event', Lang.bind(this, function (a, e) {
            // Eat button press events so they don't go through and close the pane
            return true;
        }));

        let chromeTop = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                      spacing: 6 });

        let global = Shell.Global.get();
        let closeIconUri = "file://" + global.imagedir + "close.svg";
        let closeIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
                                                                       closeIconUri,
                                                                       16,
                                                                       16);
        closeIcon.reactive = true;
        closeIcon.connect('button-press-event', Lang.bind(this, function (b, e) {
            this.close();
            return true;
        }));
        chromeTop.append(closeIcon, Big.BoxPackFlags.END);
        this.actor.append(chromeTop, Big.BoxPackFlags.NONE);

        this.content = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                     spacing: DEFAULT_PADDING });
        this.actor.append(this.content, Big.BoxPackFlags.EXPAND);

        // Hidden by default
        this.actor.hide();
    },

    open: function () {
        if (this._open)
            return;
        this._open = true;
        this.actor.show();
        this.emit('open-state-changed', this._open);
    },

    close: function () {
        if (!this._open)
            return;
        this._open = false;
        this.actor.hide();
        this.emit('open-state-changed', this._open);
    },

    destroyContent: function() {
        let children = this.content.get_children();
        for (let i = 0; i < children.length; i++) {
            children[i].destroy();
        }
    },

    toggle: function () {
        if (this._open)
            this.close();
        else
            this.open();
    }
}
Signals.addSignalMethods(Pane.prototype);

function ResultArea(displayClass, enableNavigation) {
    this._init(displayClass, enableNavigation);
}

ResultArea.prototype = {
    _init : function(displayClass, enableNavigation) {
        this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
        this.resultsContainer = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                              spacing: DEFAULT_PADDING
                                            });
        this.actor.append(this.resultsContainer, Big.BoxPackFlags.EXPAND);
        this.navContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
        this.resultsContainer.append(this.navContainer, Big.BoxPackFlags.NONE);

        this.display = new displayClass();

        this.navArea = this.display.getNavigationArea();
        if (enableNavigation && this.navArea)
            this.navContainer.append(this.navArea, Big.BoxPackFlags.EXPAND);

        this.resultsContainer.append(this.display.actor, Big.BoxPackFlags.EXPAND);

        this.controlBox = new Big.Box({ x_align: Big.BoxAlignment.CENTER });
        this.controlBox.append(this.display.displayControl, Big.BoxPackFlags.NONE);
        this.actor.append(this.controlBox, Big.BoxPackFlags.EXPAND);

        this.display.load();
    }
}

// Utility function shared between ResultPane and the DocDisplay in the main dash.
// Connects to the detail signal of the display, and on-demand creates a new
// pane.
function createPaneForDetails(dash, display, detailsWidth) {
    let detailPane = null;
    display.connect('show-details', Lang.bind(this, function(display, index) {
        if (detailPane == null) {
            detailPane = new Pane();
            detailPane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) {
                if (!isOpen) {
                    /* Ensure we don't keep around large preview textures */
                    detailPane.destroyContent();
                }
            }));
            dash._addPane(detailPane);
        }

        if (index >= 0) {
            detailPane.destroyContent();
            let details = display.createDetailsForIndex(index, detailsWidth, -1);
            detailPane.content.append(details, Big.BoxPackFlags.EXPAND);
            detailPane.open();
        } else {
            detailPane.close();
        }
    }));
    return null;
}

function ResultPane(dash, detailsWidth) {
    this._init(dash, detailsWidth);
}

ResultPane.prototype = {
    __proto__: Pane.prototype,

    _init: function(dash, detailsWidth) {
        Pane.prototype._init.call(this);
        this._dash = dash;
        this._detailsWidth = detailsWidth;
    },

    // Create an instance of displayClass and pack it into this pane's
    // content area.  Return the displayClass instance.
    packResults: function(displayClass, enableNavigation) {
        let resultArea = new ResultArea(displayClass, enableNavigation);

        createPaneForDetails(this._dash, resultArea.display, this._detailsWidth);

        this.content.append(resultArea.actor, Big.BoxPackFlags.EXPAND);
        this.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) {
            resultArea.display.resetState();
        }));
        return resultArea.display;
    }
}

function SearchEntry() {
    this._init();
}

SearchEntry.prototype = {
    _init : function() {
        this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                   y_align: Big.BoxAlignment.CENTER,
                                   background_color: DASH_SEARCH_BG_COLOR,
                                   corner_radius: 4,
                                   spacing: DEFAULT_PADDING,
                                   padding: DEFAULT_PADDING
                                 });

        let icon = new Gio.ThemedIcon({ name: 'gtk-find' });
        let searchIconTexture = Shell.TextureCache.get_default().load_gicon(icon, 16);
        this.actor.append(searchIconTexture, Big.BoxPackFlags.NONE);

        this.pane = null;

        // We need to initialize the text for the entry to have the cursor displayed
        // in it. See http://bugzilla.openedhand.com/show_bug.cgi?id=1365
        this.entry = new Clutter.Text({ font_name: "Sans 14px",
                                        editable: true,
                                        activatable: true,
                                        singleLineMode: true,
                                        text: ""
                                      });
        this.actor.append(this.entry, Big.BoxPackFlags.EXPAND);
    },

    setPane: function (pane) {
        this._pane = pane;
    }
};
Signals.addSignalMethods(SearchEntry.prototype);

function MoreLink() {
    this._init();
}

MoreLink.prototype = {
    _init : function () {
        this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                   padding_left: DEFAULT_PADDING,
                                   padding_right: DEFAULT_PADDING });
        let global = Shell.Global.get();
        let inactiveUri = "file://" + global.imagedir + "view-more.svg";
        let activeUri = "file://" + global.imagedir + "view-more-activated.svg";
        this._inactiveIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
                                                                            inactiveUri, 29, 18);
        this._activeIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
                                                                          activeUri, 29, 18);
        this._iconBox = new Big.Box({ reactive: true });
        this._iconBox.append(this._inactiveIcon, Big.BoxPackFlags.NONE);
        this.actor.append(this._iconBox, Big.BoxPackFlags.END);

        this.pane = null;

        this._iconBox.connect('button-press-event', Lang.bind(this, function (b, e) {
            if (this.pane == null) {
                // Ensure the pane is created; the activated handler will call setPane
                this.emit('activated');
            }
            this._pane.toggle();
            return true;
        }));
    },

    setPane: function (pane) {
        this._pane = pane;
        this._pane.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) {
            this._iconBox.remove_all();
            this._iconBox.append(isOpen ? this._activeIcon : this._inactiveIcon,
                                 Big.BoxPackFlags.NONE);
        }));
    }
}

Signals.addSignalMethods(MoreLink.prototype);

function SectionHeader(title) {
    this._init(title);
}

SectionHeader.prototype = {
    _init : function (title) {
        this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
        let text = new Clutter.Text({ color: DASH_SECTION_COLOR,
                                      font_name: "Sans Bold 10px",
                                      text: title });
        this.moreLink = new MoreLink();
        this.actor.append(text, Big.BoxPackFlags.EXPAND);
        this.actor.append(this.moreLink.actor, Big.BoxPackFlags.END);
    }
}

function Dash(displayGridColumnWidth) {
    this._init(displayGridColumnWidth);
}

Dash.prototype = {
    _init : function(displayGridColumnWidth) {
        this._width = displayGridColumnWidth;

        this._detailsWidth = displayGridColumnWidth * 2;

        let global = Shell.Global.get();

        // dash and the popup panes need to be reactive so that the clicks in unoccupied places on them
        // are not passed to the transparent background underneath them. This background is used for the workspaces area when
        // the additional dash panes are being shown and it handles clicks by closing the additional panes, so that the user
        // can interact with the workspaces. However, this behavior is not desirable when the click is actually over a pane.
        //
        // We have to make the individual panes reactive instead of making the whole dash actor reactive because the width
        // of the Group actor ends up including the width of its hidden children, so we were getting a reactive object as
        // wide as the details pane that was blocking the clicks to the workspaces underneath it even when the details pane
        // was actually hidden.
        this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                   width: this._width,
                                   padding: DEFAULT_PADDING,
                                   reactive: true });

        this.dashContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                           spacing: DASH_SECTION_SPACING });
        this.actor.append(this.dashContainer, Big.BoxPackFlags.EXPAND);

        // The currently active popup display
        this._activePane = null;

        /***** Search *****/

        this._searchPane = null;
        this._searchActive = false;
        this._searchEntry = new SearchEntry();
        this.dashContainer.append(this._searchEntry.actor, Big.BoxPackFlags.NONE);

        this._searchAreaApps = null;
        this._searchAreaDocs = null;

        this._searchQueued = false;
        this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) {
            this._searchActive = this._searchEntry.text != '';
            if (this._searchQueued)
                return;
            if (this._searchPane == null) {
                this._searchPane = new ResultPane(this, this._detailsWidth);
                this._searchPane.content.append(new Clutter.Text({ color: DASH_SECTION_COLOR,
                                                                   font_name: 'Sans Bold 10px',
                                                                   text: "APPLICATIONS" }),
                                                 Big.BoxPackFlags.NONE);
                this._searchAreaApps = this._searchPane.packResults(AppDisplay.AppDisplay, false);
                this._searchPane.content.append(new Clutter.Text({ color: DASH_SECTION_COLOR,
                                                                   font_name: 'Sans Bold 10px',
                                                                   text: "RECENT DOCUMENTS" }),
                                                 Big.BoxPackFlags.NONE);
                this._searchAreaDocs = this._searchPane.packResults(DocDisplay.DocDisplay, false);
                this._addPane(this._searchPane);
                this._searchEntry.setPane(this._searchPane);
            }
            this._searchQueued = true;
            Mainloop.timeout_add(250, Lang.bind(this, function() {
                // Strip leading and trailing whitespace
                let text = this._searchEntry.entry.text.replace(/^\s+/g, "").replace(/\s+$/g, "");
                this._searchQueued = false;
                this._searchAreaApps.setSearch(text);
                this._searchAreaDocs.setSearch(text);
                if (text == '')
                    this._searchPane.close();
                else
                    this._searchPane.open();
                return false;
            }));
        }));
        this._searchEntry.entry.connect('activate', Lang.bind(this, function (se) {
            // only one of the displays will have an item selected, so it's ok to
            // call activateSelected() on all of them
            this._searchAreaApps.activateSelected();
            this._searchAreaDocs.activateSelected();
            return true;
        }));
        this._searchEntry.entry.connect('key-press-event', Lang.bind(this, function (se, e) {
            let symbol = Shell.get_event_key_symbol(e);
            if (symbol == Clutter.Escape) {
                // Escape will keep clearing things back to the desktop. First, if
                // we have active text, we remove it.
                if (this._searchEntry.entry.text != '')
                    this._searchEntry.entry.text = '';
                // Next, if we're in one of the "more" modes or showing the details pane, close them
                else if (this._activePane != null)
                    this._activePane.close();
                // Finally, just close the overlay entirely
                else
                    Main.overlay.hide();
                return true;
            } else if (symbol == Clutter.Up) {
                if (!this._searchActive)
                    return true;
                // selectUp and selectDown wrap around in their respective displays
                // too, but there doesn't seem to be any flickering if we first select
                // something in one display, but then unset the selection, and move
                // it to the other display, so it's ok to do that.
                if (this._searchAreaDocs.hasSelected())
                  this._searchAreaDocs.selectUp();
                else if (this._searchAreaApps.hasItems())
                  this._searchAreaApps.selectUp();
                else
                  this._searchAreaDocs.selectUp();
                return true;
            } else if (symbol == Clutter.Down) {
                if (!this._searchActive)
                    return true;
                if (this._searchAreaDocs.hasSelected())
                  this._searchAreaDocs.selectDown();
                else if (this._searchAreaApps.hasItems())
                  this._searchAreaApps.selectDown();
                else
                  this._searchAreaDocs.selectDown();
                return true;
            }
            return false;
        }));

        /***** Applications *****/

        let appsHeader = new SectionHeader("APPLICATIONS");
        this._appsSection = new Big.Box({ spacing: DEFAULT_PADDING });
        this._appsSection.append(appsHeader.actor, Big.BoxPackFlags.NONE);

        this._appsContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
        this._appsSection.append(this._appsContent, Big.BoxPackFlags.EXPAND);
        this._appWell = new AppDisplay.AppWell();
        this._appsContent.append(this._appWell.actor, Big.BoxPackFlags.EXPAND);

        this._moreAppsPane = null;
        appsHeader.moreLink.connect('activated', Lang.bind(this, function (link) {
            if (this._moreAppsPane == null) {
                this._moreAppsPane = new ResultPane(this, this._detailsWidth);
                this._moreAppsPane.packResults(AppDisplay.AppDisplay, true);
                this._addPane(this._moreAppsPane);
                link.setPane(this._moreAppsPane);
           }
        }));

        this.dashContainer.append(this._appsSection, Big.BoxPackFlags.NONE);

        /***** Places *****/

        let placesSection = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                          spacing: DEFAULT_PADDING });
        let placesHeader = new SectionHeader("PLACES");
        placesSection.append(placesHeader.actor, Big.BoxPackFlags.NONE);

        let placesDisplay = new Places.Places();
        placesSection.append(placesDisplay.actor, Big.BoxPackFlags.EXPAND);

        this.dashContainer.append(placesSection, Big.BoxPackFlags.NONE);

        /***** Documents *****/

        this._docsSection = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                          spacing: DEFAULT_PADDING });
        this._moreDocsPane = null;

        let docsHeader = new SectionHeader("RECENT DOCUMENTS");
        this._docsSection.append(docsHeader.actor, Big.BoxPackFlags.NONE);

        this._docDisplay = new DocDisplay.DocDisplay();
        this._docDisplay.load();
        this._docsSection.append(this._docDisplay.actor, Big.BoxPackFlags.EXPAND);

        createPaneForDetails(this, this._docDisplay, this._detailsWidth);

        docsHeader.moreLink.connect('activated', Lang.bind(this, function (link) {
            if (this._moreDocsPane == null) {
                this._moreDocsPane = new ResultPane(this, this._detailsWidth);
                this._moreDocsPane.packResults(DocDisplay.DocDisplay, true);
                this._addPane(this._moreDocsPane);
                link.setPane(this._moreDocsPane);
           }
        }));

        this.dashContainer.append(this._docsSection, Big.BoxPackFlags.EXPAND);
    },

    show: function() {
        let global = Shell.Global.get();
        global.stage.set_key_focus(this._searchEntry.entry);
    },

    hide: function() {
        this._firstSelectAfterOverlayShow = true;
        if (this._searchEntry.entry.text != '')
            this._searchEntry.entry.text = '';
        if (this._activePane != null)
            this._activePane.close();
    },

    closePanes: function () {
        if (this._activePane != null)
            this._activePane.close();
    },

    _addPane: function(pane) {
        pane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) {
            if (isOpen) {
                if (pane != this._activePane && this._activePane != null) {
                    this._activePane.close();
                }
                this._activePane = pane;
            } else if (pane == this._activePane) {
                this._activePane = null;
            }
        }));
        Main.overlay.addPane(pane);
    }
};
Signals.addSignalMethods(Dash.prototype);