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

const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const St = imports.gi.St;

/**
 * Lightbox:
 * @container: parent Clutter.Container
 * @inhibitEvents: whether to inhibit events for @container
 * @width: (optional) shade actor width
 * @height: (optional) shade actor height
 *
 * Lightbox creates a dark translucent "shade" actor to hide the
 * contents of @container, and allows you to specify particular actors
 * in @container to highlight by bringing them above the shade. It
 * tracks added and removed actors in @container while the lightboxing
 * is active, and ensures that all actors are returned to their
 * original stacking order when the lightboxing is removed. (However,
 * if actors are restacked by outside code while the lightboxing is
 * active, the lightbox may later revert them back to their original
 * order.)
 *
 * By default, the shade window will have the height and width of
 * @container and will track any changes in its size. You can override
 * this by passing an explicit width and height
 */
function Lightbox(container, inhibitEvents, width, height) {
    this._init(container, inhibitEvents, width, height);
}

Lightbox.prototype = {
    _init : function(container, inhibitEvents, width, height) {
        this._container = container;
        this._children = container.get_children();
        this.actor = new St.Bin({ x: 0,
                                  y: 0,
                                  style_class: 'lightbox',
                                  reactive: inhibitEvents });

        container.add_actor(this.actor);
        this.actor.raise_top();

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

        if (width && height) {
            this.actor.width = width;
            this.actor.height = height;
            this._allocationChangedSignalId = 0;
        } else {
            this.actor.width = container.width;
            this.actor.height = container.height;
            this._allocationChangedSignalId = container.connect('allocation-changed', Lang.bind(this, this._allocationChanged));
        }

        this._actorAddedSignalId = container.connect('actor-added', Lang.bind(this, this._actorAdded));
        this._actorRemovedSignalId = container.connect('actor-removed', Lang.bind(this, this._actorRemoved));

        this._highlighted = null;
    },

    _allocationChanged : function(container, box, flags) {
        this.actor.width = this._container.width;
        this.actor.height = this._container.height;
    },

    _actorAdded : function(container, newChild) {
        let children = this._container.get_children();
        let myIndex = children.indexOf(this.actor);
        let newChildIndex = children.indexOf(newChild);

        if (newChildIndex > myIndex) {
            // The child was added above the shade (presumably it was
            // made the new top-most child). Move it below the shade,
            // and add it to this._children as the new topmost actor.
            newChild.lower(this.actor);
            this._children.push(newChild);
        } else if (newChildIndex == 0) {
            // Bottom of stack
            this._children.unshift(newChild);
        } else {
            // Somewhere else; insert it into the correct spot
            let prevChild = this._children.indexOf(children[newChildIndex - 1]);
            if (prevChild != -1) // paranoia
                this._children.splice(prevChild + 1, 0, newChild);
        }
    },

    _actorRemoved : function(container, child) {
        let index = this._children.indexOf(child);
        if (index != -1) // paranoia
            this._children.splice(index, 1);

        if (child == this._highlighted)
            this._highlighted = null;
    },

    /**
     * highlight:
     * @window: actor to highlight
     *
     * Highlights the indicated actor and unhighlights any other
     * currently-highlighted actor. With no arguments or a false/null
     * argument, all actors will be unhighlighted.
     */
    highlight : function(window) {
        if (this._highlighted == window)
            return;

        // Walk this._children raising and lowering actors as needed.
        // Things get a little tricky if the to-be-raised and
        // to-be-lowered actors were originally adjacent, in which
        // case we may need to indicate some *other* actor as the new
        // sibling of the to-be-lowered one.

        let below = this.actor;
        for (let i = this._children.length - 1; i >= 0; i--) {
            if (this._children[i] == window)
                this._children[i].raise_top();
            else if (this._children[i] == this._highlighted)
                this._children[i].lower(below);
            else
                below = this._children[i];
        }

        this._highlighted = window;
    },

    /**
     * destroy:
     *
     * Destroys the lightbox.
     */
    destroy : function() {
        this.actor.destroy();
    },

    /**
     * _onDestroy:
     *
     * This is called when the lightbox' actor is destroyed, either
     * by destroying its container or by explicitly calling this.destroy().
     */
    _onDestroy: function() {
        if (this._allocationChangedSignalId != 0)
            this._container.disconnect(this._allocationChangedSignalId);
        this._container.disconnect(this._actorAddedSignalId);
        this._container.disconnect(this._actorRemovedSignalId);

        this.highlight(null);
    }
};