# Source: core/tree-controller.js

    var Montage = require("./core").Montage,
        WeakMap = require("collections/weak-map"),
        parse = require("frb/parse"),
        evaluate = require("frb/evaluate");

    var TreeNode = exports.TreeNode = Montage.specialize({

        constructor: {
            value: function (data, controller) {
                this.data = data;
                this.controller = controller;
            }
        },

        isExpanded: {
            get: function () {
                return this.controller.isNodeExpanded(this.data);
            },
            set: function (value) {
                if (value) {
                    this.controller.expandNode(this.data);
                } else {
                    this.controller.collapseNode(this.data);
                }
            }
        }

    });

    exports.TreeController = Montage.specialize({

        constructor: {
            value: function () {
                this._listenersHash = {};
                this._listenersCounter = 0;
            }
        },

        _childrenExpressionProperty: {
            value: "children"
        },

        _childrenExpression: {
            value: null
        },

        /**
         * An FRB expression, that evaluated against content or any of its
         * children, produces an array of that content's children.
         * By default it is "children"
         */
        childrenExpression: {
            get: function () {
                if (this._childrenExpression === null) {
                    return "children";
                }
                return this._childrenExpression;
            },
            set: function (value) {
                if (this._childrenExpression !== value) {
                    var parsedValue = null;

                    this._childrenExpression = value;
                    if (value) {

                        // We parse the given childrenExpression...

                        if (typeof value === "string") {
                            parsedValue = parse(value);
                        }

                        // ...and if it is just a property we set _childrenExpressionProperty
                        //  to be used later for speed optimisation purposes

                        if ((parsedValue !== null) &&
                            (parsedValue.type === "property") &&
                            (parsedValue.args) &&
                            (parsedValue.args.length === 2) &&
                            (parsedValue.args[0].type === "value") &&
                            (parsedValue.args[1].type === "literal")) {
                            this._childrenExpressionProperty = parsedValue.args[1].value;
                        } else {
                            this._childrenExpressionProperty = null;
                        }
                    } else {
                        this._childrenExpressionProperty = "children";
                    }
                }
            }
        },

        /**
         * Whether nodes in the tree should be expanded by default.
         * This will only apply when the tree's [data]{@link TreeController#data}
         * property is set. Nodes added dynamically will ignore this flag.
         *
         * @type boolean
         */
        initiallyExpanded: {
            value: false
        },

        _data: {
            value: null
        },

        /**
         * Tree data model / root
         */
        data: {
            get: function () {
                return this._data;
            },
            set: function (value) {
                if (this._data !== value) {
                    this._expansionMap = new WeakMap();
                    this._data = value;
                    if (this.initiallyExpanded) {
                        this.expandAll();
                    } else {
                        this.handleTreeChange();
                    }
                }
            }
        },

        /**
         * Calls handleTreeChange in delegate when nodes
         * are expanded/collapsed and when data changes
         */
        handleTreeChange: {
            value: function () {
                if (!this._isOwnUpdate) {
                    this._updateListeners();
                    if (this.delegate && this.delegate.handleTreeChange) {
                        this.delegate.handleTreeChange();
                    }
                }
            }
        },

        _expandNode: {
            value: function (node) {
                if (!this.isNodeExpanded(node)) {
                    this._expansionMap.set(node, {});
                    return true;
                }
                return false;
            }
        },

        /**
         * Expands a given node of the tree
         */
        expandNode: {
            value: function (node) {
                if (typeof node === "object" && this._expandNode(node)) {
                    this.handleTreeChange();
                    return true;
                }
                return false;
            }
        },

        _expandAll: {
            value: function (node) {
                var childrenData = this.childrenFromNode(node),
                    length,
                    i;

                if (childrenData) {
                    length = childrenData.length;
                    if (length) {
                        this._expandNode(node);
                        for (i = 0; i < length; i++) {
                            this._expandAll(childrenData[i]);
                        }
                    }
                }
            }
        },

        /**
         * Expands all nodes with children in the tree.
         */
        expandAll: {
            value: function () {
                if (this._data) {
                    this._expandAll(this._data);
                    this.handleTreeChange();
                }
            }
        },

        _collapseNode: {
            value: function (node) {
                return this._expansionMap.delete(node);
            }
        },

        /**
         * Collapses a given node of the tree
         */
        collapseNode: {
            value: function (node) {
                if (typeof node === "object" && this._collapseNode(node)) {
                    this.handleTreeChange();
                    return true;
                }
                return false;
            }
        },

        /**
         * Collapses all nodes with children in the tree.
         */
        collapseAll: {
            value: function () {
                this._expansionMap = new WeakMap();
                this.handleTreeChange();
                return true;
            }
        },

        /**
         * Gets the node expansion value - boolean - for a given node
         */
        isNodeExpanded: {
            value: function (node) {
                return this._expansionMap.has(node);
            }
        },

        /**
         * Returns the children of a given node based on childrenExpression
         */
        childrenFromNode: {
            value: function (node) {

                // This is a speed optimisation. If childrenExpression
                // is just a single property, we don't evaluate it

                if (this._childrenExpressionProperty === null) {
                    return evaluate(this._childrenExpression, node);
                }
                return node[this._childrenExpressionProperty];
            }
        },

        _getReachableExpandedNodes: {
            value: function (node, result) {
                var expansionMetadata,
                    children,
                    length,
                    i;

                if (!result) {
                    result = [];
                }
                if (node && (expansionMetadata = this._expansionMap.get(node))) {
                    result.push(node);
                    children = this.childrenFromNode(node);
                    if (children) {
                        length = children.length;
                        for (i = 0; i < length; i++) {
                            this._getReachableExpandedNodes(children[i], result);
                        }
                    }
                }
                return result;
            }
        },

        _addListener: {
            value: function (expandedNode) {
                var expansionMetadata = this._expansionMap.get(expandedNode),
                    treeNode,
                    cancelListener;

                this._isOwnUpdate = true;
                treeNode = new TreeNode(expandedNode, this);
                cancelListener = treeNode.addRangeAtPathChangeListener(
                    this._childrenExpression ? "data.path(controller._childrenExpression)" : "data.children",
                    this,
                    "handleTreeChange"
                );
                this._isOwnUpdate = false;
                this._listenersCounter++;
                this._listenersHash[this._listenersCounter] = {
                    cancelListener: cancelListener,
                    node: expandedNode
                };
                expansionMetadata.listenerId = this._listenersCounter;
                return this._listenersCounter;
            }
        },

        _removeListener: {
            value: function (id) {
                var expandedNode = this._listenersHash[id].node,
                    expansionMetadata = this._expansionMap.get(expandedNode);

                this._listenersHash[id].cancelListener();
                if (expansionMetadata) {
                    delete expansionMetadata.listenerId;
                }
            }
        },

        _updateListeners: {
            value: function () {
                var reachableExpandedNodes = this._getReachableExpandedNodes(this._data),
                    expansionMetadata,
                    length = reachableExpandedNodes.length,
                    listenersHash = {},
                    listenersIds = [],
                    id,
                    i;

                for (i = 0; i < length; i++) {
                    expansionMetadata = this._expansionMap.get(reachableExpandedNodes[i]);
                    if (expansionMetadata.listenerId) {
                        listenersIds.push(expansionMetadata.listenerId);
                    } else {
                        listenersIds.push(
                            this._addListener(reachableExpandedNodes[i])
                        );
                    }
                    listenersHash[expansionMetadata.listenerId] = this._listenersHash[expansionMetadata.listenerId];
                    delete this._listenersHash[expansionMetadata.listenerId];
                }
                for (id in this._listenersHash) {
                    if (this._listenersHash.hasOwnProperty(id)) {
                        this._removeListener(id);
                    }
                }
                this._listenersHash = listenersHash;
            }
        }

    });