Show:
/* Copyright © 2015-2016 David Valdman */

define(function(require, exports, module) {
    var Transform = require('../core/Transform');
    var Transitionable = require('../core/Transitionable');
    var View = require('../core/View');
    var LayoutNode = require('../core/LayoutNode');
    var Stream = require('../streams/Stream');
    var Differential = require('../streams/Differential');
    var Accumulator = require('../streams/Accumulator');
    var EventMapper = require('../events/EventMapper');

    var CONSTANTS = {
        DIRECTION : {
            X : 0,
            Y : 1
        },
        SIDE : {
            LEFT : 0,
            TOP : 1,
            RIGHT : 2,
            BOTTOM : 3
        },
        ORIENTATION : {
            POSITIVE :  1,
            NEGATIVE : -1
        }
    };

    /**
     * A layout composed of two sections: content and drawer.
     *
     *  The drawer is initially hidden behind the content, until it is moved
     *  by a call to setPosition. The source of the movement can be by subscribing
     *  the layout to user input (like a Mouse/Touch/Scroll input), or by manually
     *  calling setPosition with a transition.
     *
     *  The layout emits a `start`, `update` and `end` Stream with payload
     *
     *      `progress` - Number between 0 and 1 indicating how open the drawer is
     *      `value` - Pixel displacement in how open the drawer is
     *
     *  It also emits `close` and `open` events.
     *
     *  The drawer can be revealed from any side of the content (top, left, bottom, right),
     *  by specifying a side option.
     *
     *  @class DrawerLayout
     *  @constructor
     *  @namespace Layouts
     *  @extends Core.View
     *  @param [options] {Object}                       Options
     *  @param [options.side] {Number}                  Side to reveal the drawer from. Defined in DrawerLayout.SIDES
     *  @param [options.revealLength] {Number}          The maximum length to reveal the drawer
     *  @param [options.velocityThreshold] {Number}     The velocity needed to complete the drawer transition
     *  @param [options.positionThreshold] {Number}     The displacement needed to complete the drawer transition
     *  @param [options.transitionClose] {Object}       A transition definition for closing the drawer
     *  @param [options.transitionOpen] {Object}        A transition definition for opening the drawer
     */
    var DrawerLayout = View.extend({
        defaults : {
            side : CONSTANTS.SIDE.LEFT,
            revealLength : undefined,
            velocityThreshold : Infinity,
            positionThreshold : 0,
            transitionOpen : true,
            transitionClose : true
        },
        events : {
            change : _updateState
        },
        initialize : function initialize(options){
            // DERIVED STATE

            // vertical or horizontal movement
            this.direction = _getDirectionFromSide(options.side);

            // positive or negative movement along the direction
            this.orientation = _getOrientationFromSide(options.side);

            // scale the revealLength by the parity of the direction
            this.options.revealLength *= this.orientation;

            // open state (needed for toggling)
            this.isOpen = false;

            // STREAMS
            
            // responsible for manually moving the content without user input
            this.transitionStream = new Transitionable(0);

            // responsible for moving the content from user input
            var gestureDelta = new Stream({
                start : function (){
                    this.transitionStream.halt();
                    return 0;
                }.bind(this),
                update : function (data){
                    // modify the delta from user input to be constrained
                    // by the revealLength
                    var delta = data.delta;
                    var newDelta = delta;
                    var revealLength = options.revealLength;

                    var currentPosition = this.position.get();
                    var newPosition = currentPosition + delta;

                    var MIN_LENGTH = 0;
                    var MAX_LENGTH = 0;

                    if (this.orientation === CONSTANTS.ORIENTATION.POSITIVE)
                        MAX_LENGTH = revealLength;
                    else
                        MIN_LENGTH = revealLength;

                    if (newPosition >= MAX_LENGTH || newPosition <= MIN_LENGTH){
                        if (newPosition > MAX_LENGTH && newPosition > MIN_LENGTH && currentPosition !== MAX_LENGTH)
                            newDelta = MAX_LENGTH - currentPosition;
                        else if (newPosition < MIN_LENGTH && currentPosition !== MIN_LENGTH)
                            newDelta = MIN_LENGTH - currentPosition;
                        else
                            newDelta = 0;
                    }

                    return newDelta;
                }.bind(this),
                end : function (data){
                    var velocity = data.velocity;

                    var orientation = this.orientation;
                    var position = this.position.get();

                    var length = options.revealLength;
                    var MAX_LENGTH = orientation * length;
                    var positionThreshold = options.positionThreshold || MAX_LENGTH / 2;
                    var velocityThreshold = options.velocityThreshold;

                    if (position === 0) {
                        this.isOpen = false;
                        return false;
                    }

                    if (position === MAX_LENGTH) {
                        this.isOpen = true;
                        return false;
                    }

                    var shouldOpen =
                        (position >= positionThreshold) && ((velocity > -velocityThreshold) || (velocity > velocityThreshold)) ||
                        (position <  positionThreshold) && ((velocity >  velocityThreshold));

                    if (shouldOpen){
                        this.options.transitionOpen.velocity = velocity;
                        this.open(this.options.transitionOpen, function(){
                            this.options.transitionOpen.velocity = 0;
                        }.bind(this));
                    }
                    else {
                        this.options.transitionClose.velocity = velocity;
                        this.close(this.options.transitionClose, function(){
                            this.options.transitionClose.velocity = 0;
                        }.bind(this));
                    }
                }.bind(this)
            });

            gestureDelta.subscribe(this.input);

            var transitionDelta = new Differential();
            transitionDelta.subscribe(this.transitionStream);

            this.position = new Accumulator(0);
            this.position.subscribe(gestureDelta);
            this.position.subscribe(transitionDelta);

            var outputMapper = new EventMapper(function(position){
                return {
                    value : position,
                    progress : position / this.options.revealLength
                }
            }.bind(this));

            this.output.subscribe(outputMapper).subscribe(this.position);
        },
        /**
         * Set the drawer component with a Surface of View.
         *
         * @method addDrawer
         * @param drawer {Surface|View}
         */
        addDrawer : function addDrawer(drawer){
            if (this.options.revealLength == undefined)
                this.options.revealLength = drawer.getSize()[this.direction];

            this.drawer = drawer;
            var layout = new LayoutNode({transform : Transform.behind});
            this.add(layout).add(this.drawer);
        },
        /**
         * Set the content component with a Surface or View.
         *
         * @method addContent
         * @param content {Surface|View}
         */
        addContent : function addContent(content){
            var transform = this.position.map(function(position){
                return (this.direction === CONSTANTS.DIRECTION.X)
                    ? Transform.translateX(position)
                    : Transform.translateY(position)
            }.bind(this));

            var layout = new LayoutNode({transform : transform});

            this.add(layout).add(content);
        },
        /**
         * Reveals the drawer with a transition.
         *   Emits an `open` event when an opening transition has been committed to.
         *
         * @method open
         * @param [transition] {Boolean|Object} transition definition
         * @param [callback] {Function}         callback
         */
        open : function open(transition, callback){
            if (transition === undefined) transition = this.options.transitionOpen;
            this.setPosition(this.options.revealLength, transition, callback);
            if (!this.isOpen) {
                this.isOpen = true;
                this.emit('open');
            }
        },
        /**
         * Conceals the drawer with a transition.
         *   Emits a `close` event when an closing transition has been committed to.
         *
         * @method close
         * @param [transition] {Boolean|Object} transition definition
         * @param [callback] {Function}         callback
         */
        close : function close(transition, callback){
            if (transition === undefined) transition = this.options.transitionClose;
            this.setPosition(0, transition, callback);
            if (this.isOpen){
                this.isOpen = false;
                this.emit('close');
            }
        },
        /**
         * Toggles between open and closed states.
         *
         * @method toggle
         * @param [transition] {Boolean|Object} transition definition
         */
        toggle : function toggle(transition){
            if (this.isOpen) this.close(transition);
            else this.open(transition);
        },
        /**
         * Sets the position in pixels for the content's displacement.
         *
         * @method setPosition
         * @param position {Number}             position
         * @param [transition] {Boolean|Object} transition definition
         * @param [callback] {Function}         callback
         */
        setPosition : function setPosition(position, transition, callback) {
            this.transitionStream.reset(this.position.get());
            this.transitionStream.set(position, transition, callback);
        },
        /**
         * Resets to last state of being open or closed
         *
         * @method reset
         * @param [transition] {Boolean|Object} transition definition
         */
        reset : function reset(transition) {
            if (this.isOpen) this.open(transition);
            else this.close(transition);
        }
    }, CONSTANTS);

    function _getDirectionFromSide(side) {
        var SIDE = CONSTANTS.SIDE;
        var DIRECTION = CONSTANTS.DIRECTION;
        return (side === SIDE.LEFT || side === SIDE.RIGHT)
            ? DIRECTION.X
            : DIRECTION.Y;
    }

    function _getOrientationFromSide(side) {
        var SIDES = CONSTANTS.SIDE;
        return (side === SIDES.LEFT || side === SIDES.TOP)
            ? CONSTANTS.ORIENTATION.POSITIVE
            : CONSTANTS.ORIENTATION.NEGATIVE;
    }

    function _updateState(data){
        var key = data.key;
        var value = data.value;
        if (key !== 'side') {
            this.direction = _getDirectionFromSide(value);
            this.orientation = _getOrientationFromSide(value);
        }
        this.options.revealLength *= this.direction;
    }

    module.exports = DrawerLayout;
});