//
//  Backbone-associations.js 0.6.1
//
//  (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
//  Backbone-associations may be freely distributed under the MIT license.
//  For all details and documentation:
//  https://github.com/dhruvaray/backbone-associations/
//

(function(root, factory) {
    // Set up Backbone-associations appropriately for the environment. Start with AMD.
    if (typeof define === 'function' && define.amd) {
        define(['underscore', 'backbone'], function(_, Backbone) {
            // Export global even in AMD case in case this script is loaded with
            // others that may still expect a global Backbone.
            return factory(root, Backbone, _);
        });

    // Next for Node.js or CommonJS.
    } else if (typeof exports !== 'undefined') {
        var _ = require('underscore'),
            Backbone = require('backbone');
        factory(root, Backbone, _);
        if (typeof module !== 'undefined' && module.exports) {
            module.exports = Backbone;
        }
        exports = Backbone;

    // Finally, as a browser global.
    } else {
        factory(root, root.Backbone, root._);
    }

}(this, function(root, Backbone, _) {
    "use strict";

    // Initial Setup
    // --------------

    // The top-level namespace. All public Backbone classes and modules will be attached to this.
    // Exported for the browser and CommonJS.
    var BackboneModel, BackboneCollection, ModelProto, BackboneEvent,
        CollectionProto, AssociatedModel, pathChecker,
        delimiters, pathSeparator, sourceModel, sourceKey, endPoints = {};

    // Create local reference `Model` prototype.
    BackboneModel = Backbone.Model;
    BackboneCollection = Backbone.Collection;
    ModelProto = BackboneModel.prototype;
    CollectionProto = BackboneCollection.prototype;
    BackboneEvent = Backbone.Events;

    Backbone.Associations = {
        VERSION: "0.6.1"
    };

    // Alternative scopes other than root
    Backbone.Associations.scopes = [];

    // Define `getter` and `setter` for `separator`
    var getSeparator = function() {
        return pathSeparator;
    };
    // Define `setSeperator`
    var setSeparator = function(value) {
        if (!_.isString(value) || _.size(value) < 1) {
            value = ".";
        }
        // set private properties
        pathSeparator = value;
        pathChecker = new RegExp("[\\" + pathSeparator + "\\[\\]]+", "g");
        delimiters = new RegExp("[^\\" + pathSeparator + "\\[\\]]+", "g");
    };

    try {
        // Define `SEPERATOR` property to Backbone.Associations
        Object.defineProperty(Backbone.Associations, 'SEPARATOR', {
            enumerable: true,
            get: getSeparator,
            set: setSeparator
        });
    } catch (e) {}

    // Backbone.AssociatedModel
    // --------------

    //Add `Many` and `One` relations to Backbone Object.
    Backbone.Associations.Many = Backbone.Many = "Many";
    Backbone.Associations.One = Backbone.One = "One";
    Backbone.Associations.Self = Backbone.Self = "Self";
    // Set default separator
    Backbone.Associations.SEPARATOR = ".";
    Backbone.Associations.getSeparator = getSeparator;
    Backbone.Associations.setSeparator = setSeparator;

    Backbone.Associations.EVENTS_BUBBLE = true;
    Backbone.Associations.EVENTS_WILDCARD = true;
    Backbone.Associations.EVENTS_NC = false;


    setSeparator();

    // Define `AssociatedModel` (Extends Backbone.Model).
    AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
        // Define relations with Associated Model.
        relations:undefined,
        // Define `Model` property which can keep track of already fired `events`,
        // and prevent redundant event to be triggered in case of cyclic model graphs.
        _proxyCalls:undefined,

        // Override constructor to set parents
        constructor: function (attributes, options) {
            // Set parent's opportunistically.
            options && options.__parents__ && (this.parents = [options.__parents__]);
            BackboneModel.apply(this, arguments);
        },

        on: function (name, callback, context) {

            var result = BackboneEvent.on.apply(this, arguments);

            // No optimization possible if nested-events is wanted by the application
            if (Backbone.Associations.EVENTS_NC) return result;

            // Regular expression used to split event strings.
            var eventSplitter = /\s+/;

            // Handle atomic event names only
            if (_.isString(name) && name && (!eventSplitter.test(name)) && callback) {
                var endPoint = getPathEndPoint(name);
                if (endPoint) {
                    //Increment end point counter. Represents # of nodes which listen to this end point
                    endPoints[endPoint] = (typeof endPoints[endPoint] === 'undefined') ? 1 : (endPoints[endPoint] + 1);
                }
            }
            return result;
        },

        off: function (name, callback, context) {

            // No optimization possible if nested-events is wanted by the application
            if (Backbone.Associations.EVENTS_NC) return BackboneEvent.off.apply(this, arguments);

            var eventSplitter = /\s+/,
                events = this._events,
                listeners = {},
                names = events ? _.keys(events) : [],
                all = (!name && !callback && !context),
                atomic_event = (_.isString(name) && (!eventSplitter.test(name)));

            if (all || atomic_event) {
                for (var i = 0, l = names.length; i < l; i++) {
                    // Store the # of callbacks listening to the event name prior to the `off` call
                    listeners[names[i]] = events[names[i]] ? events[names[i]].length : 0;
                }
            }
            // Call Backbone off implementation
            var result = BackboneEvent.off.apply(this, arguments);

            if (all || atomic_event) {
                for (i = 0, l = names.length; i < l; i++) {
                    var endPoint = getPathEndPoint(names[i]);
                    if (endPoint) {
                        if (events[names[i]]) {
                            // Some listeners wiped out for this name for this object
                            endPoints[endPoint] -= (listeners[names[i]] - events[names[i]].length);
                        } else {
                            // All listeners wiped out for this name for this object
                            endPoints[endPoint] -= listeners[names[i]];
                        }
                    }
                }
            }
            return result;
        },

        // Get the value of an attribute.
        get:function (attr) {
            var cache = this.__attributes__,
                val = ModelProto.get.call(this, attr),
                obj = cache ? val || cache[attr] : val;
            return obj ? obj : this._getAttr.apply(this, arguments);
        },

        // Set a hash of model attributes on the Backbone Model.
        set:function (key, value, options) {
            var attributes, result;
            // Duplicate backbone's behavior to allow separate key/value parameters,
            // instead of a single 'attributes' object.
            if (_.isObject(key) || key == null) {
                attributes = key;
                options = value;
            } else {
                attributes = {};
                attributes[key] = value;
            }
            result = this._set(attributes, options);
            // Trigger events which have been blocked until the entire object graph is updated.
            this._processPendingEvents();
            return result;

        },

        // Works with an attribute hash and options + fully qualified paths
        _set:function (attributes, options) {
            var attr, modelMap, modelId, obj, result = this;
            if (!attributes) return this;

            // temp cache of attributes
            this.__attributes__ = attributes;

            for (attr in attributes) {
                //Create a map for each unique object whose attributes we want to set
                modelMap || (modelMap = {});
                if (attr.match(pathChecker)) {
                    var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
                        last = pathTokens[pathTokens.length - 1],
                        parentModel = this.get(initials);
                    if (parentModel instanceof BackboneModel) {
                        obj = modelMap[parentModel.cid] ||
                            (modelMap[parentModel.cid] = {'model': parentModel, 'data': {}});
                        obj.data[last] = attributes[attr];
                    }
                } else {
                    obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
                    obj.data[attr] = attributes[attr];
                }
            }

            if (modelMap) {
                for (modelId in modelMap) {
                    obj = modelMap[modelId];
                    this._setAttr.call(obj.model, obj.data, options) || (result = false);

                }
            } else {
                result = this._setAttr.call(this, attributes, options);
            }
            delete this.__attributes__;

            return result;

        },

        // Set a hash of model attributes on the object,
        // fire Backbone `event` with options.
        // It maintains relations between models during the set operation.
        // It also bubbles up child events to the parent.
        _setAttr:function (attributes, options) {
            var attr;
            // Extract attributes and options.
            options || (options = {});
            if (options.unset) for (attr in attributes) attributes[attr] = void 0;
            this.parents = this.parents || [];

            if (this.relations) {
                // Iterate over `this.relations` and `set` model and collection values
                // if `relations` are available.
                _.each(this.relations, function (relation) {
                    var relationKey = relation.key,
                        activationContext = relation.scope || root,
                        relatedModel = this._transformRelatedModel(relation, attributes),
                        collectionType = this._transformCollectionType(relation, relatedModel, attributes),
                        map = _.isString(relation.map) ? map2Scope(relation.map, activationContext) : relation.map,
                        currVal = this.attributes[relationKey],
                        idKey = currVal && currVal.idAttribute,
                        val, relationOptions, data, relationValue, newCtx = false;

                    // Merge in `options` specific to this relation.
                    relationOptions = relation.options ? _.extend({}, relation.options, options) : options;

                    if (attributes[relationKey]) {

                        // Get value of attribute with relation key in `val`.
                        val = _.result(attributes, relationKey);

                        // Map `val` if a transformation function is provided.
                        val = map ? map.call(this, val, collectionType ? collectionType : relatedModel) : val;
                        if(!val) {
                            attributes[relationKey] = val;
                            return;
                        }

                        // If `relation.type` is `Backbone.Many`,
                        // Create `Backbone.Collection` with passed data and perform Backbone `set`.
                        if (relation.type === Backbone.Many) {

                            if (currVal) {
                                // Setting this flag will prevent events from firing immediately. That way clients
                                // will not get events until the entire object graph is updated.
                                currVal._deferEvents = true;

                                // Use Backbone.Collection's `reset` or smart `set` method
                                currVal[relationOptions.reset ? 'reset' : 'set'](
                                    val instanceof BackboneCollection ? val.models : val, relationOptions);

                                data = currVal;

                            } else {
                                newCtx = true;

                                if (val instanceof BackboneCollection) {
                                    data = val;
                                } else {
                                    data = this._createCollection(
                                        collectionType || BackboneCollection,
                                        relation.collectionOptions || (relatedModel ? {model: relatedModel} : {})
                                    );
                                    data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
                                }
                            }

                        } else if (relation.type === Backbone.One) {

                            var hasOwnProperty = (val instanceof BackboneModel) ?
                                val.attributes.hasOwnProperty(idKey) :
                                val.hasOwnProperty(idKey);
                            var newIdKey = (val instanceof BackboneModel) ?
                                val.attributes[idKey] :
                                val[idKey];

                            //Is the passed in data for the same key?
                            if (currVal && hasOwnProperty &&
                                currVal.attributes[idKey] === newIdKey) {
                                // Setting this flag will prevent events from firing immediately. That way clients
                                // will not get events until the entire object graph is updated.
                                currVal._deferEvents = true;
                                // Perform the traditional `set` operation
                                currVal._set(val instanceof BackboneModel ? val.attributes : val, relationOptions);
                                data = currVal;
                            } else {
                                newCtx = true;

                                if (val instanceof BackboneModel) {
                                    data = val;
                                } else {
                                    relationOptions.__parents__ = this;
                                    data = new relatedModel(val, relationOptions);
                                    delete relationOptions.__parents__;
                                }

                            }
                        } else {
                            throw new Error('type attribute must be specified and ' +
                                'have the values Backbone.One or Backbone.Many');
                        }


                        attributes[relationKey] = data;
                        relationValue = data;

                        // Add proxy events to respective parents.
                        // Only add callback if not defined or new Ctx has been identified.
                        if (newCtx || (relationValue && !relationValue._proxyCallback)) {
                            if(!relationValue._proxyCallback) {
                              relationValue._proxyCallback = function () {
                                  return Backbone.Associations.EVENTS_BUBBLE &&
                                      this._bubbleEvent.call(this, relationKey, relationValue, arguments);
                              };
                            }
                            relationValue.on("all", relationValue._proxyCallback, this);
                        }

                    }
                    //Distinguish between the value of undefined versus a set no-op
                    if (attributes.hasOwnProperty(relationKey))
                        this._setupParents(attributes[relationKey], this.attributes[relationKey]);
                }, this);
            }
            // Return results for `BackboneModel.set`.
            return  ModelProto.set.call(this, attributes, options);
        },


        // Bubble-up event to `parent` Model
        _bubbleEvent:function (relationKey, relationValue, eventArguments) {
            var args = eventArguments,
                opt = args[0].split(":"),
                eventType = opt[0],
                catch_all = args[0] == "nested-change",
                isChangeEvent = eventType === "change",
                eventObject = args[1],
                indexEventObject = -1,
                _proxyCalls = relationValue._proxyCalls,
                cargs,
                eventPath = opt[1],
                eSrc = !eventPath || (eventPath.indexOf(pathSeparator) == -1),
                basecolEventPath;


            // Short circuit the listen in to the nested-graph event
            if (catch_all) return;

            // Record the source of the event
            if (eSrc) sourceKey = (getPathEndPoint(args[0]) || relationKey);

            // Short circuit the event bubbling as there are no listeners for this end point
            if (!Backbone.Associations.EVENTS_NC && !endPoints[sourceKey]) return;

            // Short circuit the listen in to the wild-card event
            if (Backbone.Associations.EVENTS_WILDCARD) {
                if (/\[\*\]/g.test(eventPath)) return this;
            }

            if (relationValue instanceof BackboneCollection && (isChangeEvent || eventPath)) {
                // O(n) search :(
                indexEventObject = relationValue.indexOf(sourceModel || eventObject);
            }

            if (this instanceof BackboneModel) {
                // A quicker way to identify the model which caused an update inside the collection (while bubbling up)
                sourceModel = this;
            }
            // Manipulate `eventPath`.
            eventPath = relationKey + ((indexEventObject !== -1 && (isChangeEvent || eventPath)) ?
                "[" + indexEventObject + "]" : "") + (eventPath ? pathSeparator + eventPath : "");

            // Short circuit collection * events

            if (Backbone.Associations.EVENTS_WILDCARD) {
                basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
            }

            cargs = [];
            cargs.push.apply(cargs, args);
            cargs[0] = eventType + ":" + eventPath;

            // Create a collection modified event with wild-card
            if (Backbone.Associations.EVENTS_WILDCARD && eventPath !== basecolEventPath) {
                cargs[0] = cargs[0] + " " + eventType + ":" + basecolEventPath;
            }

            // If event has been already triggered as result of same source `eventPath`,
            // no need to re-trigger event to prevent cycle.
            _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
            if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;

            // Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
            _proxyCalls[eventPath] = true;


            // Set up previous attributes correctly.
            if (isChangeEvent) {
                this._previousAttributes[relationKey] = relationValue._previousAttributes;
                this.changed[relationKey] = relationValue;
            }


            // Bubble up event to parent `model` with new changed arguments.

            this.trigger.apply(this, cargs);

            //Only fire for change. Not change:attribute
            if (Backbone.Associations.EVENTS_NC && isChangeEvent && this.get(eventPath) != args[2]) {
                var ncargs = ["nested-change", eventPath, args[1]];
                args[2] && ncargs.push(args[2]); //args[2] will be options if present
                this.trigger.apply(this, ncargs);
            }

            // Remove `eventPath` from `_proxyCalls`,
            // if `eventPath` and `_proxyCalls` are available,
            // which allow event to be triggered on for next operation of `set`.
            if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];

            sourceModel = undefined;

            return this;
        },

        // Has event been fired from this source. Used to prevent event recursion in cyclic graphs
        _isEventAvailable:function (_proxyCalls, path) {
            return _.find(_proxyCalls, function (value, eventKey) {
                return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
            });
        },

        //Maintain reverse pointers - a.k.a parents
        _setupParents: function (updated, original) {
            // Set new parent for updated
            if (updated) {
                updated.parents = updated.parents || [];
                (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
            }
            // Remove `this` from the earlier set value's parents (if the new value is different).
            if (original && original.parents.length > 0 && original != updated) {
                original.parents = _.difference(original.parents, [this]);
                // Don't bubble to this parent anymore
                original._proxyCallback && original.off("all", original._proxyCallback, this);
            }
        },

        // Returns new `collection` (or derivatives) of type `options.model`.
        _createCollection: function (type, options) {
            options = _.defaults(options, {model: type.model});
            var c = new type([], _.isFunction(options) ? options.call(this) : options);
            c.parents = [this];
            return c;
        },

        // Process all pending events after the entire object graph has been updated
        _processPendingEvents:function () {
            if (!this._processedEvents) {
                this._processedEvents = true;

                this._deferEvents = false;

                // Trigger all pending events
                _.each(this._pendingEvents, function (e) {
                    e.c.trigger.apply(e.c, e.a);
                });

                this._pendingEvents = [];

                // Traverse down the object graph and call process pending events on sub-trees
                _.each(this.relations, function (relation) {
                    var val = this.attributes[relation.key];
                    val && val._processPendingEvents && val._processPendingEvents();
                }, this);

                delete this._processedEvents;
            }
        },

        // Process the raw `relatedModel` value set in a relation
        _transformRelatedModel: function (relation, attributes) {

            var relatedModel = relation.relatedModel;
            var activationContext = relation.scope || root;

            // Call function if relatedModel is implemented as a function
            if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
                relatedModel = _.isFunction(relatedModel) ?
                    relatedModel.call(this, relation, attributes) :
                    relatedModel;

            // Get class if relation and map is stored as a string.
            if (relatedModel && _.isString(relatedModel)) {
                relatedModel = (relatedModel === Backbone.Self) ?
                    this.constructor :
                    map2Scope(relatedModel, activationContext);
            }

            // Error checking
            if (relation.type === Backbone.One) {

                if (!relatedModel)
                    throw new Error('specify a relatedModel for Backbone.One type');

                if (!(relatedModel.prototype instanceof Backbone.Model))
                    throw new Error('specify an AssociatedModel or Backbone.Model for Backbone.One type');
            }

            return relatedModel;
        },

        // Process the raw `collectionType` value set in a relation
        _transformCollectionType: function (relation, relatedModel, attributes) {

            var collectionType = relation.collectionType;
            var activationContext = relation.scope || root;

            if (collectionType && _.isFunction(collectionType) &&
                (collectionType.prototype instanceof BackboneModel))
                throw new Error('type is of Backbone.Model. Specify derivatives of Backbone.Collection');

            // Call function if collectionType is implemented as a function
            if (collectionType && !(collectionType.prototype instanceof BackboneCollection))
                collectionType = _.isFunction(collectionType) ?
                    collectionType.call(this, relation, attributes) : collectionType;

            collectionType && _.isString(collectionType) &&
            (collectionType = map2Scope(collectionType, activationContext));

            // `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
            if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
                throw new Error('collectionType must inherit from Backbone.Collection');
            }

            // Error checking
            if (relation.type === Backbone.Many) {
                if ((!relatedModel) && (!collectionType))
                    throw new Error('specify either a relatedModel or collectionType');
            }

            return collectionType;

        },

        // Override trigger to defer events in the object graph.
        trigger:function (name) {
            // Defer event processing
            if (this._deferEvents) {
                this._pendingEvents = this._pendingEvents || [];
                // Maintain a queue of pending events to trigger after the entire object graph is updated.
                this._pendingEvents.push({c:this, a:arguments});
            } else {
                ModelProto.trigger.apply(this, arguments);
            }
        },

        // The JSON representation of the model.
        toJSON:function (options) {
            var json = {}, aJson;
            json[this.idAttribute] = this.id;
            if (!this.visited) {
                this.visited = true;
                // Get json representation from `BackboneModel.toJSON`.
                json = ModelProto.toJSON.apply(this, arguments);

                // Pick up only the keys you want to serialize
                if (options && options.serialize_keys) {
                    json = _.pick(json, options.serialize_keys);
                }
                // If `this.relations` is defined, iterate through each `relation`
                // and added it's json representation to parents' json representation.
                if (this.relations) {
                    _.each(this.relations, function (relation) {
                        var key = relation.key,
                            remoteKey = relation.remoteKey,
                            attr = this.attributes[key],
                            serialize = !relation.isTransient,
                            serialize_keys = relation.serialize || [],
                            _options = _.clone(options);

                        // Remove default Backbone serialization for associations.
                        delete json[key];

                        //Assign to remoteKey if specified. Otherwise use the default key.
                        //Only for non-transient relationships
                        if (serialize) {

                            // Pass the keys to serialize as options to the toJSON method.
                            if (serialize_keys.length) {
                                _options ?
                                    (_options.serialize_keys = serialize_keys) :
                                    (_options = {serialize_keys: serialize_keys})
                            }

                            aJson = attr && attr.toJSON ? attr.toJSON(_options) : attr;
                            json[remoteKey || key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
                        }

                    }, this);
                }

                delete this.visited;
            }
            return json;
        },

        // Create a new model with identical attributes to this one.
        clone: function (options) {
            return new this.constructor(this.toJSON(options));
        },

        // Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly.
        // Not calling this will leak memory and have wrong parents.
        // See test case "parent relations"
        cleanup:function (options) {
            options = options || {};

            _.each(this.relations, function (relation) {
                var val = this.attributes[relation.key];
                if(val) {
                    val._proxyCallback && val.off("all", val._proxyCallback, this);
                    val.parents = _.difference(val.parents, [this]);
                }
            }, this);
            
            (!options.listen) && this.off();
        },

        // Override destroy to perform house-keeping on `parents` collection
        destroy: function(options) {
            options = options ? _.clone(options) : {};
            options = _.defaults(options, {remove_references: true, listen: true});
            var model = this;

            if(options.remove_references && options.wait) {
                // Proxy success implementation
                var success = options.success;

                // Substitute with an implementation which will remove references to `model`
                options.success = function (resp) {
                    if (success) success(model, resp, options);
                    model.cleanup(options);
                }
            }
            // Call the base implementation
            var xhr =  ModelProto.destroy.apply(this, [options]);

            if(options.remove_references && !options.wait) {
                model.cleanup(options);
            }

            return xhr;
        },

        // Navigate the path to the leaf object in the path to query for the attribute value
        _getAttr:function (path) {

            var result = this,
                cache = this.__attributes__,
            //Tokenize the path
                attrs = getPathArray(path),
                key,
                i;
            if (_.size(attrs) < 1) return;
            for (i = 0; i < attrs.length; i++) {
                key = attrs[i];
                if (!result) break;
                //Navigate the path to get to the result
                result = result instanceof BackboneCollection
                    ? (isNaN(key) ? undefined : result.at(key))
                    : (cache ? result.attributes[key] || cache[key] : result.attributes[key]);
            }
            return result;
        }
    });

    // Tokenize the fully qualified event path
    var getPathArray = function (path) {
        if (path === '') return [''];
        return _.isString(path) ? (path.match(delimiters)) : path || [];
    };

    // Get the end point of the path.
    var getPathEndPoint = function (path) {

        if (!path) return path;

        // event_type:<path>
        var tokens = path.split(":");

        if (tokens.length > 1) {
            path = tokens[tokens.length - 1];
            tokens = path.split(pathSeparator);
            return tokens.length > 1 ? tokens[tokens.length - 1].split('[')[0] : tokens[0].split('[')[0];
        } else {
            //path of 0 depth
            return "";
        }

    };

    var map2Scope = function (path, context) {
        var target,
            scopes = [context];

        // Check global scopes after passed-in context
        scopes.push.apply(scopes, Backbone.Associations.scopes);

        for (var ctx, i = 0, l = scopes.length; i < l; ++i) {
            if (ctx = scopes[i]) {
                target = _.reduce(path.split(pathSeparator), function (memo, elem) {
                    return memo[elem];
                }, ctx);
                if (target) break;
            }
        }
        return target;
    };


    // Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
    var map2models = function (parents, target, models) {
        var relation, surrogate;
        //Iterate over collection's parents
        _.find(parents, function (parent) {
            //Iterate over relations
            relation = _.find(parent.relations, function (rel) {
                return parent.get(rel.key) === target;
            }, this);
            if (relation) {
                surrogate = parent;//surrogate for transformation
                return true;//break;
            }
        }, this);

        //If we found a relation and it has a mapping function
        if (relation && relation.map) {
            return relation.map.call(surrogate, models, target);
        }
        return models;
    };

    var proxies = {};
    // Proxy Backbone collection methods
    _.each(['set', 'remove', 'reset'], function (method) {
        proxies[method] = BackboneCollection.prototype[method];

        CollectionProto[method] = function (models, options) {
            //Short-circuit if this collection doesn't hold `AssociatedModels`
            if (this.model.prototype instanceof AssociatedModel && this.parents) {
                //Find a map function if available and perform a transformation
                arguments[0] = map2models(this.parents, this, models);
            }
            return proxies[method].apply(this, arguments);
        }
    });

    // Override trigger to defer events in the object graph.
    proxies['trigger'] = CollectionProto['trigger'];
    CollectionProto['trigger'] = function (name) {
        if (this._deferEvents) {
            this._pendingEvents = this._pendingEvents || [];
            // Maintain a queue of pending events to trigger after the entire object graph is updated.
            this._pendingEvents.push({c:this, a:arguments});
        } else {
            proxies['trigger'].apply(this, arguments);
        }
    };

    // Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
    CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
    CollectionProto.on = AssociatedModel.prototype.on;
    CollectionProto.off = AssociatedModel.prototype.off;

    return Backbone;
}));
