/**
 * A collection representing the object hierarchy of a data model
 *
 * To be used only as an internal member of the "models/services/datamodel/DataModel" module
 */

define([
            'underscore',
            'collections/Base',
            'models/services/datamodel/private/Object'
        ],
        function(
            _,
            Base,
            DMObject
        ) {

    return Base.extend({

        /**
         *  A list of reserved root object names representing the abstract base classes for data model objects.
         *  Their order in this list will also dictate the order of root objects in each hierarchy instance,
         *  e.g. all root objects that inherit from BaseEvent will come first.
         */

        ROOT_OBJECT_ORDER: ['BaseEvent', 'BaseTransaction', 'BaseSearch', 'BaseInterface', 'BaseImplements'],

        model: DMObject,

        initialize: function(models, options) {
            Base.prototype.initialize.call(this, models, options);

            // a private instance member containing the hierarchy's root objects
            // used as an entry point for walking the tree
            this._rootObjects = [];

            this.on('reset change:parentName add remove', this.buildHierarchy, this);
        },

        /**
         * Triggered on any reset, add, or remove event.  Re-builds the object hierarchy by reading each object's
         * "parentName" attribute and using it to make sure that parent object has the correct list of children.
         *
         * TODO: should this method throw or trigger an error if the models are not in a valid state?
         */

        buildHierarchy: function() {
            var childrenMap,
                that = this,
                hasNoParent = function(model) {
                    return _(that.ROOT_OBJECT_ORDER).indexOf(model.get('parentName')) > -1;
                };

            // build record of root objects and parent->children relationships
            this._rootObjects = this.filter(hasNoParent);
            childrenMap = this.chain().reject(hasNoParent).groupBy(function(model) {
                return model.get('parentName');
            }).value();

            // clear out any existing child lists
            this.each(function(object) {
                delete(object.children);
            });
            // assign each child list as a member of the model, assembling the tree
            // TODO [JCS] Do we need to do this now that the backend provides a children property?
            _(childrenMap).each(function(childList, parentName) {
                childrenMap[parentName] = _(childList).sortBy(function(child) { return child.get('objectName'); });
                var parentModel = this.find(function(model) { return model.get('objectName') === parentName; });
                parentModel.children = childList;
            }, this);

            // sort the root level objects based on the "root object order" property
            this._rootObjects = _(this._rootObjects).sortBy(function(obj) {
                return _(this.ROOT_OBJECT_ORDER).indexOf(obj.get('parentName'));
            }, this);

            // populate the the rootParent field for each object
            _(this._rootObjects).each(function(root) {
                var rootParentName = root.get('parentName');
                root.set({
                    rootParent: rootParentName
                }, {silent: true});
                root.withEachDescendant(function(child) {
                    child.set({
                        rootParent: rootParentName
                    }, {silent: true});
                }, this);
            }, this);
        },

        /**
         * @param fn - the function to call with each object as its argument, expected to return an object literal
         * @param context - (optional) the scope in which to call the function
         * @return {Array}
         *
         * A tree-structure-flattening version of map
         *
         * Returns a list generated by applying the given callback to each object and then recursively to its children,
         * and also appends a 'depth' field to each item in the returned list, indicating that object's depth in the tree.
         *
         * For example with the following hierarchy structure:
         *
         * Parent
         *   --> ChildOne
         *         --> GrandChildOne
         *         --> GrandChildTwo
         *   --> ChildTwo
         *         --> GrandChildThree
         * Sibling
         *   --> ChildThree
         *
         * Calling "depthFirstMap(function(obj) { return { name: obj.get('objectName') }; });" would return:
         *
         * [
         *   {
         *     name: "Parent",
         *     depth: 0
         *   },
         *   {
         *     name: "ChildOne",
         *     depth: 1
         *   },
         *   {
         *     name: "GrandChildOne",
         *     depth: 2
         *   },
         *   {
         *     name: "GrandChildTwo",
         *     depth: 2
         *   },
         *   {
         *     name: "ChildTwo",
         *     depth: 1
         *   },
         *   {
         *     name: "GrandChildThree",
         *     depth: 2
         *   },
         *   {
         *     name: "Sibling",
         *     depth: 0
         *   },
         *   {
         *     name: "ChildThree",
         *     depth: 1
         *   }
         * ]
         */

        depthFirstMap: function(fn, context) {
            context = context || null;
            var list = [],

                recApplyFn = function(obj, depth) {
                    var mappedObj = fn.call(context, obj);
                    mappedObj.depth = depth;
                    list.push(mappedObj);
                    depth++;
                    _(obj.children).each(function(obj) {
                        recApplyFn(obj, depth);
                    });
                };

            _(this._rootObjects).each(function(obj) {
                recApplyFn(obj, 0);
            });
            return list;
        },
        /**
         * Returns an array of the root objects, sorted by ROOT_OBJECT_ORDER
         *
         * @return {Array}
         */
        getRootObjects: function() {
            return this._rootObjects;
        },

        getAncestorsForObject: function(objectName) {
            var object = this.get(objectName);
            var ancestors = [];
            var parentName = "";

            while (object && !_.contains(this.ROOT_OBJECT_ORDER, parentName)) {
                parentName = object.get("parentName");
                object = this.get(parentName);
                if (object) {
                    ancestors.push(object);
                }
            }

            return ancestors;

        }

    });

});