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

define(function(require, exports, module) {
    var Transform = require('../core/Transform');
    var View = require('../core/View');
    var ReduceStream = require('../streams/ReduceStream');
    var Stream = require('../streams/Stream');

    var CONSTANTS = {
        DIRECTION : {
            X : 0, 
            Y : 1
        }
    };

    // Default map to convert displacement to transform
    function DEFAULT_LENGTH_MAP(length){
        return (this.options.direction === CONSTANTS.DIRECTION.X)
            ? Transform.translateX(length)
            : Transform.translateY(length);
    }

    /**
     * A layout which arranges items in series based on their size.
     *  Items can be arranged vertically or horizontally.
     *
     * @class SequentialLayout
     * @constructor
     * @namespace Layouts
     * @extends Core.View
     * @param [options] {Object}                        Options
     * @param [options.direction]{Number}               Direction to lay out items
     * @param [options.spacing] {Transitionable|Number} Gutter spacing between items
     */
    var SequentialLayout = View.extend({
        defaults : {
            direction : CONSTANTS.DIRECTION.X,
            spacing : 0,
            offset : 0
        }, 
        initialize : function initialize(options) {
            // Store nodes and flex values
            this.nodes = [];

            this.stream = new ReduceStream(function(prev, size, spacing){
                if (!size) return false;
                return prev + size[options.direction] + spacing;
            }, undefined, {sources : [options.spacing], offset : options.offset});

            this.setLengthMap(DEFAULT_LENGTH_MAP);

            var length = Stream.lift(function(length, spacing){
                return Math.max(length - spacing, 0);
            }, [this.stream.headOutput, options.spacing]);

            this.output.subscribe(length);

            // SequentialLayout derives its size from its content
            var size = [];
            this.size = Stream.lift(function(parentSize, length){
                if (!parentSize || length === undefined) return;
                size[options.direction] = length;
                size[1 - options.direction] = parentSize[1 - options.direction];
                return size;
            }, [this._size, length]);
        },
        /*
        * Set a custom map from length displacements to transforms.
        * `this` will automatically be bound to the instance.
        *
        * @method setLengthMap
        * @param map [Function] Map `(length) -> transform`
        */
        setLengthMap : function(map, sources){
            this.transformMap = map.bind(this);
            if (sources) this.setSources(sources);
        },
        setSources : function(sources){
            this.sources = sources;
        },
        /*
         * Add a renderable to the end of the layout
         *
         * @method push
         * @param map [Function] Map `(length) -> transform`
         */
        push : function(item) {
            this.nodes.push(item);
            var length = this.stream.push(item.size);

            var transform = (this.sources)
                ? Stream.lift(this.transformMap, [length].concat(this.sources))
                : length.map(this.transformMap);

            this.add({transform : transform}).add(item);
        },
        /*
         * Unlink the last renderable in the layout
         *
         * @method pop
         * @return item
         */
        pop : function(){
            return this.unlink(this.nodes.length - 1);
        },
        /*
         * Add a renderable to the beginning of the layout
         *
         * @method unshift
         * @param item {Surface|View} Renderable
         */
        unshift : function(item){
            this.nodes.unshift(item);
            var length = this.stream.unshift(item.size);
            var transform = length.map(this.transformMap);
            this.add({transform : transform}).add(item);
        },
        /*
         * Unlink the first renderable in the layout
         *
         * @method shift
         * @return item
         */
        shift : function(){
            return this.unlink(0);
        },
        /*
         * Add a renderable after a specified renderable
         *
         * @method insertAfter
         * @param prevItem {NumberSurface|View} Index or renderable to insert after
         * @param item {Surface|View}           Renderable to insert
         */
        insertAfter : function(prevItem, item) {
            var index;
            if (typeof prevItem === 'number'){
                index = prevItem + 1;
                prevItem = this.nodes[prevItem];
            }
            else index = this.nodes.indexOf(prevItem) + 1;

            this.nodes.splice(index, 0, item);

            if (!prevItem) return this.push(item);
            var length = this.stream.insertAfter(prevItem.size, item.size);
            var transform = length.map(this.transformMap);
            this.add({transform : transform}).add(item);
        },
        /*
         * Add a renderable before a specified renderable
         *
         * @method insertAfter
         * @param prevItem {Number|Surface|View} Index or renderable to insert before
         * @param item {Surface|View}            Renderable to insert
         */
        insertBefore : function(postItem, item){
            var index;
            if (typeof postItem === 'number'){
                index = postItem - 1;
                postItem = this.nodes[postItem];
            }
            else index = this.nodes.indexOf(postItem) - 1;

            this.nodes.splice(index + 1, 0, item);
            
            if (!postItem) return this.unshift(item);
            var length = this.stream.insertBefore(postItem.size, item.size);
            var transform = length.map(this.transformMap);
            this.add({transform : transform}).add(item);
        },
        /*
         * Unlink the renderable.
         *  To remove the renderable, call the `.remove` method on it after unlinking.
         *
         * @method unlink
         * @param item {Surface|View} Item to remove
         * @return item
         */
        unlink : function(item){
            var index;
            if (typeof item === 'number'){
                index = item;
                item = this.nodes[item];
            }
            else index = this.nodes.indexOf(item);

            if (!item || !item.size) return;
            this.nodes.splice(index, 1);
            this.stream.remove(item.size);

            return item;
        }
    }, CONSTANTS);

    module.exports = SequentialLayout;
});