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

const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const Shell = imports.gi.Shell;
const Lang = imports.lang;
const Mainloop = imports.mainloop;

const Main = imports.ui.main;
const Tweener = imports.ui.tweener;
const Widget = imports.ui.widget;

const WIDGETBOX_BG_COLOR = new Clutter.Color();
WIDGETBOX_BG_COLOR.from_pixel(0xf0f0f0ff);
const BLACK = new Clutter.Color();
BLACK.from_pixel(0x000000ff);

const WIDGETBOX_PADDING = 2;
const ANIMATION_TIME = 0.5;
const POP_IN_LAG = 250; /* milliseconds */

function WidgetBox(widget, expanded) {
    this._init(widget, expanded);
}

WidgetBox.prototype = {
    _init: function(widget, expanded) {
        this.state = expanded ? Widget.STATE_EXPANDED : Widget.STATE_COLLAPSED;

	if (widget instanceof Widget.Widget) {
	    this._widget = widget;
            this._widget.state = this.state;
	} else {
	    let ctor = this._ctorFromName(widget);
            this._widget = new ctor(this.state);
	}

        if (!this._widget.actor)
            throw new Error("widget has no actor");
        else if (!this._widget.title && !this._widget.collapsedActor)
            throw new Error("widget has neither title nor collapsedActor");

        this.state = expanded ? Widget.STATE_EXPANDED : Widget.STATE_COLLAPSED;

        // The structure of a WidgetBox:
        //
        // The top level is a Clutter.Group, which exists to make
        // pop-out work correctly; when another widget pops out, its
        // width will increase, which will in turn cause the sidebar's
        // width to increase, which will cause the sidebar to increase
        // the width of each of its children (the WidgetBoxes). But we
        // don't want the non-popped-out widgets to expand, so we make
        // the top-level actor be a Clutter.Group, which will accept
        // the new width from the Sidebar, but not impose it on its
        // own child.
        //
        // Inside the toplevel group is a horizontal Big.Box
        // containing 2 Clutter.Groups; one for the collapsed state
        // (cgroup) and one for the expanded state (egroup). Each
        // group contains a single vertical Big.Box (cbox and ebox
        // respectively), which have the appropriate fixed width. The
        // cbox contains either the collapsed widget actor or else the
        // rotated title. The ebox contains the horizontal title (if
        // any), separator line, and the expanded widget actor. (If
        // the widget doesn't have a collapsed actor, and therefore
        // supports pop-out, then it will also have a vertical line
        // between the two groups, which will only be shown during
        // pop-out.)
        //
        // In the expanded view, cgroup is hidden and egroup is shown.
        // When animating to the collapsed view, first the ebox is
        // slid offscreen by giving it increasingly negative x
        // coordinates within egroup. Then once it's fully offscreen,
        // we hide egroup, show cgroup, and slide cbox back in in the
        // same way.
        //
        // The pop-out view works similarly to the second half of the
        // collapsed-to-expanded transition, except that the
        // horizontal title gets hidden to avoid duplication.

        this.actor = new Clutter.Group();
        this._hbox = new Big.Box({ background_color: WIDGETBOX_BG_COLOR,
                                   padding_top: WIDGETBOX_PADDING,
                                   padding_bottom: WIDGETBOX_PADDING,
                                   padding_right: WIDGETBOX_PADDING,
                                   // Left padding is here to make up for
                                   // the X offset used for the sidebar
                                   // to hide its rounded corners
                                   padding_left: 2 * WIDGETBOX_PADDING,
                                   spacing: WIDGETBOX_PADDING,
                                   corner_radius: WIDGETBOX_PADDING,
                                   orientation: Big.BoxOrientation.HORIZONTAL,
                                   reactive: true });
        this.actor.add_actor(this._hbox);

        this._cgroup = new Clutter.Group({ clip_to_allocation: true });
        this._hbox.append(this._cgroup, Big.BoxPackFlags.NONE);

        this._cbox = new Big.Box({ width: Widget.COLLAPSED_WIDTH,
                                   clip_to_allocation: true });
        this._cgroup.add_actor(this._cbox);

        if (this._widget.collapsedActor) {
            if (this._widget.collapsedActor == this._widget.actor)
                this._singleActor = true;
            else {
                this._cbox.append(this._widget.collapsedActor,
                                  Big.BoxPackFlags.NONE);
            }
        } else {
            let vtitle = new Clutter.Text({ font_name: "Sans 16px",
                                            text: this._widget.title,
                                            rotation_angle_z: -90.0 });
            let signalId = vtitle.connect('notify::allocation',
                                          function () {
                                              vtitle.disconnect(signalId);
                                              vtitle.set_anchor_point(vtitle.natural_width, 0);
                                              vtitle.set_size(vtitle.natural_height,
                                                              vtitle.natural_width);
                                          });
            this._vtitle = vtitle;
            this._cbox.append(this._vtitle, Big.BoxPackFlags.NONE);

            this._vline = new Clutter.Rectangle({ color: BLACK, width: 1 });
            this._hbox.append(this._vline, Big.BoxPackFlags.NONE);
            this._vline.hide();

            // Set up pop-out
            this._eventHandler = this._hbox.connect('captured-event',
                                                    Lang.bind(this, this._popEventHandler));
            this._activationHandler = this._widget.connect('activated',
                                                           Lang.bind(this, this._activationHandler));
        }

        this._egroup = new Clutter.Group({ clip_to_allocation: true });
        this._hbox.append(this._egroup, Big.BoxPackFlags.NONE);

        this._ebox = new Big.Box({ spacing: WIDGETBOX_PADDING,
                                   width: Widget.EXPANDED_WIDTH,
                                   clip_to_allocation: true });
        this._egroup.add_actor(this._ebox);

        if (this._widget.title) {
            this._htitle = new Clutter.Text({ font_name: "Sans 16px",
                                              text: this._widget.title });
            this._ebox.append(this._htitle, Big.BoxPackFlags.NONE);

            this._hline = new Clutter.Rectangle({ color: BLACK, height: 1 });
            this._ebox.append(this._hline, Big.BoxPackFlags.NONE);
        }

        this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);

        if (expanded)
            this._setWidgetExpanded();
        else
            this._setWidgetCollapsed();
    },

    // Given a name like "imports.ui.widget.ClockWidget", turn that
    // into a constructor function
    _ctorFromName: function(name) {
        // Make sure it's a valid import
        if (!name.match(/^imports(\.[a-zA-Z0-9_]+)+$/))
            throw new Error("widget name must start with 'imports.'");
        if (name.match(/^imports\.gi\./))
            throw new Error("cannot import widget from GIR");

        let ctor = eval(name);

        // Make sure it's really a constructor
        if (!ctor || typeof(ctor) != "function")
            throw new Error("widget name is not a constructor");

        // Make sure it's a widget
        let proto = ctor.prototype;
        while (proto && proto != Widget.Widget.prototype)
            proto = proto.__proto__;
        if (!proto)
            throw new Error("widget does not inherit from Widget prototype");

        return ctor;
    },

    expand: function() {
        Tweener.addTween(this._cbox, { x: -Widget.COLLAPSED_WIDTH,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._expandPart1Complete,
                                       onCompleteScope: this });
        this.state = this._widget.state = Widget.STATE_EXPANDING;
    },

    _setWidgetExpanded: function() {
        this._cgroup.hide();
        this._egroup.show();

        if (this._singleActor) {
            this._widget.actor.unparent();
            this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);
        }

        if (this._htitle) {
            this._htitle.show();
            this._hline.show();
        }
    },

    _expandPart1Complete: function() {
        this._cbox.x = 0;
        this._setWidgetExpanded();

        if (this._widget.expand) {
            try {
                this._widget.expand();
            } catch (e) {
                logError(e, 'Widget failed to expand');
            }
        }

        this._ebox.x = -Widget.EXPANDED_WIDTH;
        Tweener.addTween(this._ebox, { x: 0,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._expandComplete,
                                       onCompleteScope: this });
    },

    _expandComplete: function() {
        this.state = this._widget.state = Widget.STATE_EXPANDED;
    },

    collapse: function() {
        Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._collapsePart1Complete,
                                       onCompleteScope: this });
        this.state = this._widget.state = Widget.STATE_COLLAPSING;
    },

    _setWidgetCollapsed: function() {
        this._egroup.hide();
        this._cgroup.show();

        if (this._singleActor) {
            this._widget.actor.unparent();
            this._cbox.append(this._widget.actor, Big.BoxPackFlags.NONE);
        }

        if (this._htitle) {
            this._htitle.hide();
            this._hline.hide();
        }

        if (this._vtitle)
            this._cbox.height = this._ebox.height;
    },

    _collapsePart1Complete: function() {
        this._ebox.x = 0;
        this._setWidgetCollapsed();

        if (this._widget.collapse) {
            try {
                this._widget.collapse();
            } catch (e) {
                logError(e, 'Widget failed to collapse');
            }
        }

        this._cbox.x = -Widget.COLLAPSED_WIDTH;
        Tweener.addTween(this._cbox, { x: 0,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._collapseComplete,
                                       onCompleteScope: this });
    },

    _collapseComplete: function() {
        this.state = this._widget.state = Widget.STATE_COLLAPSED;
    },

    _popEventHandler: function(actor, event) {
        let type = event.type();

        if (type == Clutter.EventType.ENTER) {
            this._clearPopInTimeout();
            if (this.state == Widget.STATE_COLLAPSED ||
                this.state == Widget.STATE_COLLAPSING) {
                this._popOut();
                return false;
            }
        } else if (type == Clutter.EventType.LEAVE &&
                   (this.state == Widget.STATE_POPPED_OUT ||
                    this.state == Widget.STATE_POPPING_OUT)) {
            // If moving into another actor within this._hbox, let the
            // event be propagated
            let into = Shell.get_event_related(event);
            while (into) {
                if (into == this._hbox)
                    return false;
                into = into.get_parent();
            }

            // Else, moving out of this._hbox
            this._setPopInTimeout();
            return false;
        }

        return false;
    },

    _activationHandler: function() {
        if (this.state == Widget.STATE_POPPED_OUT)
            this._popIn();
    },

    _popOut: function() {
        if (this.state != Widget.STATE_COLLAPSED &&
            this.state != Widget.STATE_COLLAPSING)
            return;

        this._vline.show();
        this._egroup.show();
        this._ebox.x = -Widget.EXPANDED_WIDTH;
        Tweener.addTween(this._ebox, { x: 0,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._popOutComplete,
                                       onCompleteScope: this });
        this.state = this._widget.state = Widget.STATE_POPPING_OUT;

        Main.chrome.addInputRegionActor(this._hbox);
    },

    _popOutComplete: function() {
        this.state = this._widget.state = Widget.STATE_POPPED_OUT;
    },

    _setPopInTimeout: function() {
        this._clearPopInTimeout();
        this._popInTimeout = Mainloop.timeout_add(POP_IN_LAG, Lang.bind(this, function () { this._popIn(); return false; }));
    },

    _clearPopInTimeout: function() {
        if (this._popInTimeout) {
            Mainloop.source_remove(this._popInTimeout);
            delete this._popInTimeout;
        }
    },

    _popIn: function() {
        this._clearPopInTimeout();

        if (this.state != Widget.STATE_POPPED_OUT &&
            this.state != Widget.STATE_POPPING_OUT)
            return;

        Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
                                       time: ANIMATION_TIME / 2,
                                       transition: "easeOutQuad",
                                       onComplete: this._popInComplete,
                                       onCompleteScope: this });
    },

    _popInComplete: function() {
        this.state = this._widget.state = Widget.STATE_COLLAPSED;
        this._vline.hide();
        this._egroup.hide();
        this._ebox.x = 0;

        Main.chrome.removeInputRegionActor(this._hbox);
    },

    destroy: function() {
        if (this._widget.destroy)
            this._widget.destroy();
    }
};