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

define(function(require, exports, module) {
    var EventHandler = require('../../events/EventHandler');
    var SimpleStream = require('../../streams/SimpleStream');
    var Stream = require('../../streams/Stream');
    var LayoutNode = require('./LayoutNode');
    var SizeNode = require('./SizeNode');
    var layoutAlgebra = require('../algebras/layout');
    var sizeAlgebra = require('../algebras/size');
    var preTickQueue = require('../../core/queues/preTickQueue');
    var dirtyQueue = require('../../core/queues/dirtyQueue');

    /**
     * A node in the render tree. As such, it wraps a layout or size node,
     *  providing them with an `add` method. By adding nodes, the render tree
     *  is constructed, the leaves of which are `Surfaces`.
     *
     *  @constructor
     *  @class RenderTreeNode
     *  @private
     *  @param object {Object|SizeNode|LayoutNode|Surface|View}
     */
    function RenderTreeNode(object) {
        // layout and size inputs
        this._layout = new EventHandler();
        this._size = new EventHandler();
        this._logic = new EventHandler();

        // layout and size streams
        this.size = new SimpleStream();
        this.layout = new SimpleStream();

        // set node middleware
        if (object) _set.call(this, object);
        else {
            // if no middleware specified, connect input to output
            this.layout.subscribe(this._layout);
            this.size.subscribe(this._size);
        }

        // save last spec if node is removed and later added
        this._cachedSpec = {
            layout : null,
            size : null
        };

        // update size cache
        this.size.on('start', updateSizeCache.bind(this));
        this.size.on('update', updateSizeCache.bind(this));
        this.size.on('end', updateSizeCache.bind(this));

        // update layout spec
        this.layout.on('start', updateLayoutCache.bind(this));
        this.layout.on('update', updateLayoutCache.bind(this));
        this.layout.on('end', updateLayoutCache.bind(this));

        // reference to RootNode if a node is removed and later added
        this.root = null;

        this._logic.on('mount', function(node){
            this.root = node;
        }.bind(this));

        this._logic.on('unmount', function() {
            this.root = null;
        }.bind(this));
    }

    function updateLayoutCache(layout){
        this._cachedSpec.layout = layout;
    }

    function updateSizeCache(size){
        this._cachedSpec.size = size;
    }

    /**
     * Extends the render tree with a new node. Similar to how a tree data structure
     *  is created, but instead of a node with an array of children, children subscribe
     *  to notifications from the parent.
     *
     *  Nodes can be instances of `LayoutNode`, `SizeNode`, or Object literals with
     *  size and layout properties, in which case, appropriate nodes will be created.
     *
     *  This method also takes `Views` (subtrees) and `Surfaces` (leaves).
     *
     * @method add
     * @chainable
     * @param node {Object|Node|Surface|View} Node
     * @return {RenderTreeNode}
     */
    RenderTreeNode.prototype.add = function add(node) {
        var childNode;

        if (node.constructor === Object){
            // Object literal case
            return _createNodeFromObjectLiteral.call(this, node);
        }
        else if (node._isView){
            // View case
            return this.add(node._node);
        }
        else if (node instanceof RenderTreeNode){
            // RenderTree Node
            childNode = node;
        }
        else {
            // LayoutNode or SizeNode or Surface
            childNode = new RenderTreeNode(node);
        }

        childNode._layout.subscribe(this.layout);
        childNode._size.subscribe(this.size);
        childNode._logic.subscribe(this._logic);

        // Called when node is removed and later added
        if (this.root && !childNode.root)
            childNode._logic.trigger('mount', this.root);

        // Emit previously cached values if node was removed
        if (!node.root){
            var self = this;
            preTickQueue.push(function(){
                if (!self._cachedSpec.size) return;
                self.size.trigger('start', self._cachedSpec.size);
                self.layout.trigger('start', self._cachedSpec.layout);
                dirtyQueue.push(function(){
                    self.size.trigger('end', self._cachedSpec.size);
                    self.layout.trigger('end', self._cachedSpec.layout);
                });
            });
        }

        return childNode;
    };

    /**
     * Remove the node from the Render Tree
     *
     * @method remove
     */
    RenderTreeNode.prototype.remove = function (){
        this._logic.trigger('unmount');
        this._layout.unsubscribe();
        this._size.unsubscribe();
        this._logic.unsubscribe();
    };

    // Creates a combination of Size/Layout nodes from an object literal
    // depending on its keys
    function _createNodeFromObjectLiteral(object){
        var sizeKeys = {};
        var layoutKeys = {};

        var needsSize = false;
        var needsLayout = false;

        var node = this;

        for (var key in object){
            if (SizeNode.KEYS[key]){
                sizeKeys[key] = object[key];
                needsSize = true;
            }
            else if (LayoutNode.KEYS[key]){
                layoutKeys[key] = object[key];
                needsLayout = true;
            }
        }

        // create extra align node if needed
        if (needsSize && layoutKeys.align){
            var alignNode = new LayoutNode({
                align : layoutKeys.align
            });
            delete layoutKeys.align;
            node = node.add(alignNode);
        }

        // create size node first if needed
        if (needsSize)
            node = node.add(new SizeNode(sizeKeys));

        // create layout node if needed
        if (needsLayout)
            node = node.add(new LayoutNode(layoutKeys));

        return node;
    }

    // Set node middleware. Can be an object, SizeNode, LayoutNode, or Surface
    function _set(object) {
        if (object instanceof SizeNode){
            var size = Stream.lift(
                function SGSizeAlgebra (objectSpec, parentSize){
                    if (!parentSize) return false;
                    return(objectSpec)
                        ? sizeAlgebra(objectSpec, parentSize)
                        : parentSize;
                }.bind(this),
                [object, this._size]
            );
            this.size.subscribe(size);
            this.layout.subscribe(this._layout);
        }
        else if (object instanceof LayoutNode){
            var layout = Stream.lift(
                function SGLayoutAlgebra (objectSpec, parentSpec, size){
                    if (!parentSpec || !size) return false;
                    return (objectSpec)
                        ? layoutAlgebra(objectSpec, parentSpec, size)
                        : parentSpec;
                }.bind(this),
                [object, this._layout, this._size]
            );
            this.layout.subscribe(layout);
            this.size.subscribe(this._size);
        }
        else {
            this._logic.on('unmount', function() {
                object.remove();
            }.bind(this));

            this._logic.on('mount', function(root) {
                object.setup(root.allocator);
            }.bind(this));

            object.on('recall', function(){
                object._size.unsubscribe(this._size);
                object._layout.unsubscribe(this._layout);
            }.bind(this));

            object.on('deploy', function(){
                object._size.subscribe(this._size);
                object._layout.subscribe(this._layout);
            }.bind(this));
        }
    }

    module.exports = RenderTreeNode;
});