// Copyright (c) Microsoft.  All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
// files (the "Software"), to deal  in the Software without restriction, including without limitation the rights  to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/// File: jquery.observable.js
(function ($) {
    var observableFactoryKey = "__observableFactory__";  // TODO: Uniquify further?

    $.observable = function (data, options) {
        var factory = $.data(data, observableFactoryKey),
			useDefault = options && !!options.useDefault;
        // TODO: "useDefault" is used by data stores/caches when they are making changes to their
        // own cache -- changes that they don't want to interpret as an app-specified data change.
        // Consider a different design here that achieves the same effect.

        if (factory && !useDefault) {
            return factory(data);
        } else {
            return createObservable(data);
        }
    };

    $.observable.track = function (data, options) {
        if (options && $.data(data, observableFactoryKey)) {
            throw "Observable already tracked for this data.";
        }

        // TODO: Should there be an explicit "untrack" to remove the extension?
        var factory = !options ? null : function (data) {
            var observable = createObservable(data);
            observable._beforeChange = options.beforeChange;
            observable._afterChange = options.afterChange;
            observable._afterEvent = options.afterEvent;
            return observable;
        };

        $.data(data, observableFactoryKey, factory);
    };

    var splice = [].splice;

    function ObjectObservable(data) {
        if (!this.data) {
            return new ObjectObservable(data);
        }

        this._data = data;
        return this;
    }

    $.observable.Object = ObjectObservable;

    ObjectObservable.prototype = {
        _data: null,

        data: function () {
            return this._data;
        },

        setProperty: function (path, value) {
            if ($.isArray(path)) {
                // This is the array format generated by serializeArray.
                // TODO: We've discussed an "objectchange" event to capture all N property updates here.
                for (var i = 0, l = path.length; i < l; i++) {
                    var pair = path[i];
                    this.setProperty(pair.name, pair.value);
                }
            } else if (typeof (path) === "object") {
                // Object representation where property name is path and property value is value.
                // TODO: We've discussed an "objectchange" event to capture all N property updates here.
                for (var key in path) {
                    this.setProperty(key, path[key]);
                }
            } else {
                // Simple single property case.
                var setter, property,
					object = this._data,
					leaf = getLeafObject(object, path);

                path = leaf[1];
                leaf = leaf[0];
                if (leaf) {
                    property = leaf[path];
                    if ($.isFunction(property)) {
                        // Case of property setter/getter - with convention that property() is getter and property( value ) is setter
                        setter = property;
                        property = property.call(leaf); //get
                    }

                    var eventData = { path: path, value: value },
						changeFn = function (eventData) {
						    if (setter) {
						        setter.call(leaf, value);  //set
						        eventData.value = setter.call(leaf); //get updated value
						    } else {
						        leaf[path] = value;
						    }
						    return eventData;
						};
                    changeDataAndTriggerEvents(this, changeFn, $(object), "propertyChange", eventData);
                }
            }
            return this;
        }
    };

    function getLeafObject(object, path) {
        if (object && path) {
            var parts = path.split(".");

            path = parts.pop();
            while (object && parts.length) {
                object = object[parts.shift()];
            }
            return [object, path];
        }
        return [];
    }

    function ArrayObservable(data) {
        if (!this.data) {
            return new ArrayObservable(data);
        }

        this._data = data;
        return this;
    }

    function validateIndex(index) {
        if (typeof index !== "number" || index < 0) {
            throw "Invalid index.";
        }
    }

    $.observable.Array = ArrayObservable;

    ArrayObservable.prototype = {
        _data: null,

        data: function () {
            return this._data;
        },

        insert: function (index, data) {
            validateIndex(index);

            if (arguments.length > 1) {
                data = $.isArray(data) ? data : [data];  // TODO: Clone array here?
                // data can be a single item (including a null/undefined value) or an array of items.

                if (data.length > 0) {
                    var that = this,
						changeFn = function () {
						    splice.apply(that._data, [index, 0].concat(data));
						},
						eventData = { change: "insert", index: index, items: data };
                    this._changeDataAndTriggerEvents(changeFn, eventData);
                }
            }

            return this;
        },

        remove: function (index, numToRemove) {
            validateIndex(index);

            numToRemove = (numToRemove === undefined || numToRemove === null) ? 1 : numToRemove;
            if (numToRemove) {
                var that = this,
					changeFn = function () {
					    that._data.splice(index, numToRemove);
					},
					items = this._data.slice(index, index + numToRemove),
					eventData = { change: "remove", index: index, items: items };
                this._changeDataAndTriggerEvents(changeFn, eventData);
            }

            return this;
        },

        move: function (oldIndex, newIndex, numToMove) {
            validateIndex(oldIndex);
            validateIndex(newIndex);

            numToMove = (numToMove === undefined || numToMove === null) ? 1 : numToMove;
            if (numToMove) {
                var that = this,
					items = this._data.slice(oldIndex, oldIndex + numToMove),
					changeFn = function () {
					    that._data.splice(oldIndex, numToMove);
					    that._data.splice.apply(that._data, [newIndex, 0].concat(items));
					},
					eventData = { change: "move", oldIndex: oldIndex, newIndex: newIndex, items: items };
                this._changeDataAndTriggerEvents(changeFn, eventData);
            }

            return this;
        },

        refresh: function (newItems) {
            var that = this,
				changeFn = function () {
				    that._data.splice.apply(that._data, [0, that._data.length].concat(newItems));
				},
				eventData = { change: "refresh", oldItems: this._data.slice(0), newItems: newItems };
            this._changeDataAndTriggerEvents(changeFn, eventData);
            return this;
        },

        _changeDataAndTriggerEvents: function (changeFn, eventData) {
            changeDataAndTriggerEvents(this, changeFn, $([this._data]), "arrayChange", eventData);
        }
    };

    function createObservable(data) {
        return $.isArray(data) ?
            new ArrayObservable(data) : 
            new ObjectObservable(data);
        // To handle non-Array-typed collections, we could allow developers to
        // customize this logic.
    }

    function changeDataAndTriggerEvents(observable, change, $target, type, data) {
        var target = $target[0];

        if (observable._beforeChange) {
            observable._beforeChange.call(observable, target, type, data);
        }

        var newData = change(data);
        if (newData) {
            data = newData;
        }

        if (observable._afterChange) {
            observable._afterChange.call(observable, target, type, data);
        }

        $target.triggerHandler(type, data);

        if (observable._afterEvent) {
            observable._afterEvent.call(observable, target, type, data);
        }
    }

})(jQuery);

(function (global, undefined) {

    function initializeProperties(target, members) {
        for (var key in members) {
            target[key] = members[key];
        }
    }

    function defineNamespace(name) {
        var names = name.split(".");
        var current = global;
        for (var i = 0; i < names.length; i++) {
            var ns = current[names[i]];
            if (!ns || typeof ns !== "object") {
                current[names[i]] = ns = {};
            }
            current = ns;
        }
        return current;
    }

    function defineClass(ctor, instanceMembers, staticMembers) {
        ctor = ctor || function () { };
        if (instanceMembers) {
            initializeProperties(ctor.prototype, instanceMembers);
        }
        if (staticMembers) {
            initializeProperties(ctor, staticMembers);
        }
        return ctor;
    }

    function deriveClass(baseClass, ctor, instanceMembers, staticMembers) {
        var basePrototype = baseClass.prototype;
        var prototype = {};
        initializeProperties(prototype, basePrototype);
        if (instanceMembers) {
            for (var name in instanceMembers) {
                // Check if we're overwriting an existing function
                prototype[name] = typeof instanceMembers[name] === "function" && typeof basePrototype[name] === "function" ?
                    (function (name, fn) {
                        return function () {
                            var tmp = this._super;
                            this._super = basePrototype;
                            var ret = fn.apply(this, arguments);
                            this._super = tmp;
                            return ret;
                        };
                    })(name, instanceMembers[name])
                    : instanceMembers[name];
            }
        }

        ctor = ctor ?
            (function (fn) {
                return function () {
                    var tmp = this._super;
                    this._super = basePrototype;
                    fn.apply(this, arguments);
                    this._super = tmp;
                };
            })(ctor)
            : function () { };

        ctor.prototype = prototype;
        ctor.prototype.constructor = ctor;

            // copy base static
        initializeProperties(ctor, baseClass);
        if (staticMembers) {
            // copy additional static
            initializeProperties(ctor, staticMembers);
        }
        return ctor;
    }

    // pre-defined ns
    var MSD = defineNamespace("Microsoft.ServiceModel.DomainServices");

    // pre-defined routines
    MSD.defineNamespace = defineNamespace;
    MSD.defineClass = defineClass;
    MSD.deriveClass = deriveClass;

})(this);
/// <reference path="AssociatedEntitiesDataSource.js" />
/// <dictionary target='comment'>enqueue</dictionary>
(function (global, MSD, undefined) {

    function track(data, options) {
        if (!options) {
            $.observable.track(data, null);
        } else {
            $.observable.track(data, {
                beforeChange: options.beforeChange,
                afterChange: options.afterChange,
                afterEvent: options.afterEvent
            });
        }
    }

    function createInsertDeferredEvent(array, index, items) {
        return function () {
            var eventArguments = {
                change: "insert",
                index: index,
                items: items
            };
            $([array]).triggerHandler("arrayChange", eventArguments);
        };
    }

    function createRemoveDeferredEvent(array, index, itemsRemoved) {
        return function () {
            var eventArguments = {
                change: "remove",
                index: index,
                items: itemsRemoved
            };
            $([array]).triggerHandler("arrayChange", eventArguments);
        };
    }

    function createRefreshDeferredEvent(array, oldItems, newItems) {
        return function () {
            var eventArguments = {
                change: "refresh",
                oldItems: oldItems,
                newItems: newItems
            };
            $([array]).triggerHandler("arrayChange", eventArguments);
        };
    }

    function createSetPropertyDeferredEvent(item, name, oldValue, newValue) {
        return function () {
            var eventArguments = {
                path: name,
                value: newValue
            };
            $(item).triggerHandler("propertyChange", eventArguments);
        };
    }

    function observableInsert(array, index, items) {
        $.observable(array).insert(index, items);
    }

    var Observability = MSD.defineNamespace("Microsoft.ServiceModel.DomainServices.Observability");

    Observability.Configuration = {
        track: track,

        createInsertDeferredEvent: createInsertDeferredEvent,
        createRemoveDeferredEvent: createRemoveDeferredEvent,
        createRefreshDeferredEvent: createRefreshDeferredEvent,
        createSetPropertyDeferredEvent: createSetPropertyDeferredEvent,

        observableInsert: observableInsert
    };

})(this, Microsoft.ServiceModel.DomainServices);

/// <reference path="AssociatedEntitiesDataSource.js" />
/// <dictionary target='comment'>enqueue</dictionary>

(function (global, MSD, undefined) {

    var Observability = MSD.Observability = MSD.Observability || {};

    function track(data, options) {
        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var track = Observability.Configuration && Observability.Configuration.track;
        track(data, options);
        // TODO: Support apps and UI libraries that have no observability design.
    }

    function insert(array, index, items) {
        [ ].splice.apply(array, [index, 0].concat(items));

        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var createInsertDeferredEvent = Observability.Configuration && Observability.Configuration.createInsertDeferredEvent;
        return createInsertDeferredEvent(array, index, items);
        // TODO: Support apps and UI libraries that have no observability design.
    }

    function remove(array, index, numToRemove) {
        var itemsRemoved = array.slice(index, index + numToRemove);
        array.splice(index, numToRemove);

        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var createRemoveDeferredEvent = Observability.Configuration && Observability.Configuration.createRemoveDeferredEvent;
        return createRemoveDeferredEvent(array, index, itemsRemoved);
        // TODO: Support apps and UI libraries that have no observability design.
    }

    function refresh(array, newItems) {
        var oldItems = array.slice(0);
        [ ].splice.apply(array, [0, array.length].concat(newItems));

        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var createRefreshDeferredEvent = Observability.Configuration && Observability.Configuration.createRefreshDeferredEvent;
        return createRefreshDeferredEvent(array, oldItems, newItems);
        // TODO: Support apps and UI libraries that have no observability design.
    }

    function setProperty(item, name, value) {
        var oldValue = item[name];
        item[name] = value;

        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var createSetPropertyDeferredEvent = Observability.Configuration && Observability.Configuration.createSetPropertyDeferredEvent;
        return createSetPropertyDeferredEvent(item, name, oldValue, value);
        // TODO: Support apps and UI libraries that have no observability design.
    }

    // TODO: This should be removed in favor of a platform-neutral way for our LocalDataSource
    // to convey array adds to its input.  Until then, assume this is part of this larger configuration.
    function observableInsert(array, index, items) {
        // NOTE: Observability.Configuration is expected to be established by a loaded Compat.<platform>.js.
        var observableInsert = Observability.Configuration && Observability.Configuration.observableInsert;
        observableInsert(array, index, items);
    }

    $.extend(Observability, {
        track: track,

        insert: insert,
        remove: remove,
        refresh: refresh,
        setProperty: setProperty,

        // TODO: This should be removed in favor of a platform-neutral way for our LocalDataSource
        // to convey array adds to its input.  Until then, assume this is part of this larger configuration.
        observableInsert: observableInsert
    });

})(this, Microsoft.ServiceModel.DomainServices);

(function (global, MSD, undefined) {

    var ctor = function (inputData, options) {

        this._inputData = inputData;
        this._skip = null;
        this._take = null;
        this._includeTotalCount = false;

        // State
        this._observers = [];
        this._entities = [];
        this._lastRefreshTotalEntityCount = 0;
        this._flushingDeferredEvents = false;

        var clientEntityCollection;
        if (options && options.entityCollection) {
            if (Object.prototype.toString.call(options.entityCollection) !== "[object Array]") {
                throw "Entity collection must be an array";
            }
            // This is checked in RDS and LDS, since long as AssociatedEntitiesDataSource supplies
            // its own client entities.
            //// else if (options.entityCollection.length !== 0) {
            ////     throw "NYI -- Currently, entity collection must be empty to bind to a data source.";
            //// }

            clientEntityCollection = options.entityCollection;
        }

        this._clientEntities = clientEntityCollection || [];
        var self = this;
        this._clientEntities.deleteEntity = function (entity) {
            // Non-destructive delete.
            self._deleteEntity(entity);
        };

        MSD.Observability.track(this._clientEntities, {
            afterChange: function (target, type, eventArguments) {
                // Ensures that our handler is called ahead of any "arrayChange" event listeners,
                // so the DataSource can bring itself into a consistent state _before_ any clients are notified.
                self._handleCollectionChange(eventArguments);
            },
            afterEvent: function () {
                // Ensures that we issue our events _after_ all clients have been notified of internal adds.
                self._flushDeferredEvents();
            }
        });
    };


    var instanceMembers = {

        // Public methods

        dispose: function () {
            if (this._observers) {  // Use _observers as an indicator as to whether we've been disposed.
                MSD.Observability.track(this._clientEntities, null);
                this._observers = null;
            }
        },

        addObserver: function (observer) {
            if ($.inArray(observer, this._observers) < 0) {
                this._observers.push(observer);
            }
        },

        removeObserver: function (observer) {
            this._observers = $.grep(this._observers, function (element, index) {
                return element !== observer;
            });
        },

        getEntities: function () {
            return this._clientEntities;
        },

        getTotalEntityCount: function () {
            var addedEntityCount = $.grep(this._entities, function (cachedEntity) {
                return cachedEntity.added;
            }).length;
            return this._lastRefreshTotalEntityCount + addedEntityCount;
        },

        getEntityState: function (entity) {
            if (this._getCachedEntityByEntity(entity)) {
                return this._inputData.getEntityState(entity);
            } else {
                return null;
            }
        },

        isPropertyChanged: function (entity, propertyName) {
            if (this._getCachedEntityByEntity(entity)) {
                return this._inputData.isPropertyChanged(entity, propertyName);
            } else {
                throw "Entity no longer cached in data source.";
            }
        },

        getErrors: function () {
            var self = this;
            return $.grep(this._inputData.getErrors(), function (error) {
                return !!self._getCachedEntityByEntity(error.entity);
            });
        },

        getDataContext: function () {
            throw "Unreachable";  // Abstract/pure virtual method.
        },

        getEntityValidationRules: function () {
            return this._inputData.getEntityValidationRules();
        },

        getEntityId: function (entity) {
            return this._inputData.getEntityId(entity);
        },

        // TODO -- These query set-* methods should be consolidated, passing a "settings" parameter.
        // That way, we can issue a single "needs refresh" event when the client updates the query settings.
        // TODO -- Changing query options should trigger "results stale".
        setSort: function (options) {
            throw "Unreachable";  // Abstract/pure virtual method.
        },

        setFilter: function (filter) {
            throw "Unreachable";  // Abstract/pure virtual method.
        },

        setPaging: function (options) {
            options = options || {};
            this._skip = options.skip;
            this._take = options.take;
            this._includeTotalCount = !!options.includeTotalCount;
        },

        refresh: function (options) {
            throw "Unreachable";  // Abstract/pure virtual method.
        },

        revertChange: function (entity, propertyName) {
            var cachedEntity = this._getCachedEntityByEntity(entity);
            if (cachedEntity) {
                // Revert first, so client receives non-"arrayChange" events ahead of "arrayChange" (due to _purgeEntity)
                // that might signal removal of entities from our result set.
                this._inputData.revertChange(entity, propertyName);
                if (!propertyName && cachedEntity.added) {
                    this._purgeEntity(cachedEntity.entity);
                }
            } else {
                throw "Entity no longer cached in data source.";
            }
        },

        revertChanges: function (all) {
            this._inputData.revertChanges(all);

            var uncommittedAddedEntities = $.grep(this._entities, function (cachedEntity) {
                // TODO -- Can we trust added here?  How will we sync this bit with the underlying SDS?
                return cachedEntity.added;
            }),
            self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                $.each(uncommittedAddedEntities, function (index, cachedEntity) {
                    self._purgeEntity(cachedEntity.entity, deferredEvents);
                });
            });
        },


        // Private methods

        _onPropertyChanged: function (entity, property, newValue) {
            var cachedEntity = this._getCachedEntityByEntity(entity);
            if (cachedEntity) {
                this._raisePropertyChangedEvent(entity, property, newValue);
            }
        },

        _onEntityStateChanged: function (entity, state) {
            // We keep track of added entities here, so they can be removed from this._entities on revertChanges.
            var self = this;
            $.each(this._entities, function (index, cachedEntityToClearAdded) {
                if (cachedEntityToClearAdded.added && self._inputData.getEntityState(cachedEntityToClearAdded.entity) === "Unmodified") {
                    cachedEntityToClearAdded.added = false;
                }
            });

            var cachedEntity = this._getCachedEntityByEntity(entity);

            var purgeEntity = state === "Deleted" && cachedEntity &&
            (typeof this !== MSD.LocalDataSource || cachedEntity.added);
            // LocalDataSource shouldn't remove entities w/o an explicit refresh.
            // For RemoteDataSource, this "Deleted" will be the result of a refresh or internal delete.
            // In both cases, it's ok to purge.
            if (purgeEntity) {
                // Deleting a entity that is uncommitted and only on the client.
                // TODO -- Reconsider whether we shouldn't throw here, force clients to revert instead.
                this._purgeEntity(cachedEntity.entity);
            }

            if (cachedEntity) {
                this._raiseEntityStateChangedEvent(entity, state);
            }
        },

        _raiseRefreshStartEvent: function (entities, totalCount) {
            this._raiseEvent("refreshStart");
        },

        _raiseRefreshEvent: function (entities, totalCount) {
            this._raiseEvent("refresh", entities, totalCount);
        },

        _raisePropertyChangedEvent: function (entity, property, newValue) {
            this._raiseEvent("propertyChanged", entity, property, newValue);
        },

        _raiseEntityStateChangedEvent: function (entity, state) {
            this._raiseEvent("entityStateChanged", entity, state);
        },

        _raiseEvent: function (eventType) {
            var eventArguments = [].slice.call(arguments, 1),  // "arguments" isn't an Array.
                toNotify = this._observers.slice(),
                self = this;
            $.each(toNotify, function (index, observer) {
                // TODO -- We should do a setTimeout for each callback, to keep the UI from hanging up.
                if ($.isFunction(observer[eventType])) {
                    observer[eventType].apply(self, eventArguments);
                }
            });
        },

        _getCachedEntityByEntity: function (entity) {
            return $.grep(this._entities, function (cachedEntity) {
                return cachedEntity.entity === entity;
            })[0];
        },

        _handleCollectionChange: function (eventArguments) {
            switch (eventArguments.change) {
                case "insert":
                    // We don't account for eventArguments.index here.  Our cache of this._entities is not ordered,
                    // as it just track internally added entities.  Likewise, there isn't an intuitive, predictable
                    // mapping in terms of position onto our input entity collection.

                    var entitiesToAdd = eventArguments.items;
                    if (entitiesToAdd.length > 1) {
                        throw "NYI -- Can only add a single entity to/from an array in one operation.";
                    }

                    // In EntitySet, we defer entity state events around the addition of this entity.
                    // (We do this so that all clients of this._clientEntities are informed of the addition
                    // _before_ we issue related RIA-specific events re: the added entity.)
                    // We don't have to defer here, as we don't _directly_ issue events from this DataSource
                    // with respect to this add.
                    var entityToAdd = entitiesToAdd[0];
                    this._addEntity(entityToAdd);  // Assumes add to query result and not to collection-typed relationship property.
                    break;

                case "remove":
                    var entitiesToDelete = eventArguments.items;
                    if (entitiesToDelete.length > 1) {
                        throw "NYI -- Can only remove a single entity to/from an array in one operation.";
                    }

                    // TODO -- Destructive deletes are currently NYI
                    //// this._deleteEntity(this.getEntityId(entitiesToDelete[0]));
                    throw "NYI -- Cannot apply destructive deletes to a entity collection.  Use 'deleteEntity' for non-destructive delete.";
                    break;

                default:
                    throw "NYI -- Array operation '" + eventArguments.change + "' is not supported.";
            }
        },

        _addEntity: function (entity) {
            this._entities.push({ entity: entity, added: true });
            var entities = this._inputData.getEntities();
            MSD.Observability.observableInsert(entities, entities.length, [entity]);
        },

        _deleteEntity: function (entity) {
            if (this._getCachedEntityByEntity(entity)) {
                this._inputData.getEntities().deleteEntity(entity);
            } else {
                throw "Entity no longer cached in data source.";
            }
        },

        _purgeEntity: function (entityToPurge, deferredEvents) {
            this._entities = $.grep(this._entities, function (cachedEntity) {
                return cachedEntity.entity !== entityToPurge;
            });

            var index = $.inArray(entityToPurge, this._clientEntities),
            eventFn = MSD.Observability.remove(this._clientEntities, index, 1);
            if (deferredEvents) {
                deferredEvents.deferEvent(eventFn);
            } else if (eventFn) {
                eventFn();
            }
        },

        _processFilter: function (filter) {
            var filterProperty = filter.property,
            filterValue = filter.value,
            filterOperator;
            if (!filter.operator) {
                filterOperator = "==";
            } else {
                var operatorStrings = {
                    "<": ["<", "islessthan", "lessthan", "less", "lt"],
                    "<=": ["<=", "islessthanorequalto", "lessthanequal", "lte"],
                    "==": ["==", "isequalto", "equals", "equalto", "equal", "eq"],
                    "!=": ["!=", "isnotequalto", "notequals", "notequalto", "notequal", "neq", "not"],
                    ">=": [">=", "isgreaterthanorequalto", "greaterthanequal", "gte"],
                    ">": [">", "isgreaterthan", "greaterthan", "greater", "gt"]
                },
                lowerOperator = filter.operator.toLowerCase();
                for (var op in operatorStrings) {
                    if ($.inArray(lowerOperator, operatorStrings[op]) > -1) {
                        filterOperator = op;
                        break;
                    }
                }

                if (!filterOperator) {
                    // Assume that the filter operator is a function that we'll translate directly
                    // into the GET URL.
                    filterOperator = filter.operator;
                    // throw "Unrecognized filter operator '" + filter.operator + "'.";
                }
            }

            return {
                filterProperty: filterProperty,
                filterOperator: filterOperator,
                filterValue: filterValue
            };
        },

        _flushDeferredEvents: function () {
            // Will be overridden by derived classes.
        },

        _completeRefresh: function (entities, totalCount, options) {
            // Update our total entity count.  We use this cache to track internally added entities.
            this._lastRefreshTotalEntityCount = totalCount;

            // Update our client entities.
            var changed;
            var oldEntities = this.getEntities();
            if (oldEntities.length !== entities.length) {
                changed = true;
            } else {
                $.each(oldEntities, function (index, entity) {
                    if (entity !== entities[index]) {
                        changed = true;
                        return false;
                    }
                });
            }

            if (changed) {
                var self = this;
                this._entities = $.map(entities, function (entity) {
                    var added = $.grep(self._entities, function (cachedEntity) {
                        return cachedEntity.entity === entity && cachedEntity.added;
                    }).length > 0;
                    return { entity: entity, added: added };
                });

                var eventFn = MSD.Observability.refresh(this._clientEntities, entities);
                if (eventFn) {
                    eventFn();
                }
            }

            var newClientEntities = this.getEntities(),
            newTotalCount = this.getTotalEntityCount();
            if (options && options.completed && $.isFunction(options.completed)) {
                options.completed(newClientEntities, newTotalCount);
            }
            this._raiseRefreshEvent(newClientEntities, newTotalCount);
        },

        // TODO -- This code is largely duplicated in DataContext.js.
        _executeWithDeferredEvents: function (toExecute) {
            this._assertNotFlushingDeferredEvents();

            var deferredEvents = [],
            deferredEventsCollector = {
                deferEvent: function (eventToDefer) {
                    if (eventToDefer) {
                        deferredEvents.push(eventToDefer);
                    }
                }
            },
            result = toExecute(deferredEventsCollector);

            try {
                this._flushingDeferredEvents = true;
                $.each(deferredEvents, function (index, deferredEvent) {
                    deferredEvent();
                });
            } finally {
                this._flushingDeferredEvents = false;
            }

            return result;
        },

        _assertNotFlushingDeferredEvents: function () {
            if (this._flushingDeferredEvents) {
                // This catches bugs in our implementation where we try to do event deferral
                // in the process of issuing event callbacks.
                // It would also be triggered when a client tries to make a side-effecting API
                // call while we're flushing our deferred events.
                // TODO -- We should strengthen this further to catch more cases where the client
                // makes side-effecting API calls during event callbacks.  This only catches the
                // offenses during deferred event callbacks.
                throw "Issuing side-effecting operations during event callbacks is not supported.";
            }
        }
    };

    MSD.DataSource = MSD.defineClass(ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// <reference path="AssociatedEntitiesDataSource.js" />
/// <dictionary target='comment'>enqueue</dictionary>

(function (global, MSD, undefined) {

    var ctor = function (dataContext, entityType) {

        this._dataContext = dataContext;
        this._entityType = entityType;

        this._observers = [];
        this._updatedEntities = [];
        this._originalEntities = [];
        this._entityStates = {};
        this._addedEntities = [];
        this._errors = [];
        this._clientEntities = [];
        this._deferredEvents = [];
        this._childEntitiesCollections = {};

        var self = this;
        MSD.Observability.track(this._clientEntities, {
            afterChange: function (target, type, eventArguments) {
                // Ensures that our handler is called ahead of any "arrayChange" event listeners,
                // so the EntitySet can bring itself into a consistent state _before_ any clients are notified.
                self._handleCollectionChange(eventArguments);
            },
            afterEvent: function (target, type, eventArguments) {
                // Ensures that we issue our events _after_ all clients have been notified of internal adds.
                self._flushDeferredEventsFromAddOrUpdate();
            }
        });

        this._clientEntities.deleteEntity = function (entity) {
            // Non-destructive delete.
            self._deleteEntity(entity);
        };
    };


    var instanceMembers = {

        // Public methods

        dispose: function () {
            if (this._observers) {
                MSD.Observability.track(this._clientEntities, null);
                this._observers = null;
            }
        },

        // TODO -- We should align with jQuery binding/events here.
        addObserver: function (observer) {
            if ($.inArray(observer, this._observers) < 0) {
                this._observers.push(observer);
            }
        },

        removeObserver: function (observer) {
            this._observers =
            $.grep(this._observers, function (element, index) {
                return element !== observer;
            });
        },

        getEntities: function () {
            return this._clientEntities;
        },

        getEntityState: function (entity) {
            var id = this.getEntityId(entity);
            if (id) {
                return this._entityStates[id];
            }

            return null;
        },

        getEntityId: function (entity) {
            var addedEntity = this._getAddedEntityFromEntity(entity);
            if (addedEntity) {
                return addedEntity.clientId;
            }

            var index = $.inArray(entity, this._updatedEntities);
            if (index >= 0) {
                // Trust only the property values on the original entity, allowing the client to update id properties.
                return this._getServerEntityId(this._originalEntities[index]).toString();
            }

            return null;
        },

        isPropertyChanged: function (entity, propertyName) {
            var id = this.getEntityId(entity);
            switch (this._entityStates[id]) {
                case "ClientUpdated":
                case "ServerUpdating":
                    var index = this._getEntityIndexFromId(id);
                    return this._originalEntities[index][propertyName] !== this._updatedEntities[index][propertyName];
                    // TODO -- Only works for scalar-typed property values.
                    // TODO -- Check if propertyName should exist on entity according to metadata?
                    // TODO -- Extend support to parent entity properties.

                case undefined:
                case "Deleted":
                    throw "Entity no longer cached in data source.";

                default:
                    return false;
            }
        },

        getErrors: function () {
            return this._errors;
        },

        revertChange: function (entity, propertyName) {
            var id = this.getEntityId(entity);

            var state = this._entityStates[id];
            if (!state || state === "Deleted") {
                throw "Entity no longer cached in data source.";
            }

            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                if (!propertyName) {
                    if (state === "ClientDeleted" || state === "ClientUpdated") {
                        self._revertToOriginalEntity(id, deferredEvents);
                        self._errors = $.grep(self._errors, function (unused, error) { return error.entity !== entity; });
                        self._updateEntityState(id, "Unmodified", deferredEvents);
                        // TODO -- Might we sniff the entity and revert to "ClientUpdated" from "ClientDeleted" for
                        // a entity where a property has been updated then the entity deleted?  Confusing?
                    } else if (state === "ClientAdded") {
                        self._purgeUncommittedAddedEntity(self._getAddedEntityFromId(id), true, deferredEvents);
                    } else {
                        throw "Entity changes cannot be reverted for entity in state '" + state + "'.";
                    }
                } else {
                    if (state !== "ClientUpdated") {
                        throw "Property change cannot be reverted for entity in state '" + state + "'.";
                    } else {
                        var index = self._getEntityIndexFromId(id),
                        originalValue = self._originalEntities[index][propertyName];
                        var deferredEvent = MSD.Observability.setProperty(entity, propertyName, originalValue);
                        deferredEvents.deferEvent(deferredEvent);
                        self._handlePropertyChangeInternal(entity, propertyName, originalValue, deferredEvents);
                        // TODO -- Only works for scalar-typed property values.
                        // TODO -- Check if propertyName should exist on entity according to metadata?
                        // TODO -- Should we consider reverting from "ClientUpdated" to "Unmodified" for the last such change?
                        // TODO -- Should we reason over errors and GC those pertaining to propertyName here?
                        // TODO -- Extend support to parent entity properties?
                    }
                }
            });
        },

        revertChanges: function () {
            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                self.__revertChanges(deferredEvents);
            });
        },

        // Internal methods

        __hasUncommittedEdits: function () {
            var hasUncommittedEdits = false;
            $.each(this._entityStates, function (key, value) {
                if (value !== "Unmodified") {
                    hasUncommittedEdits = true;
                }
            });
            return hasUncommittedEdits;
        },

        __loadEntities: function (entities, deferredEvents) {
            // For each entity, either merge it with a cached entity or add it to the cache.
            var self = this,
            mergedLoadedEntities = [],
            entitiesNewToEntitySet = [],
            indexToInsertClientEntities = this._updatedEntities.length;
            // Re: indexToInsertClientEntities, by convention, _clientEntities are layed out as updated followed by
            // added entities.
            $.each(entities, function (unused, entity) {
                var updatedEntity,
                serverId = self._getServerEntityId(entity),
                index = self._getEntityIndexFromServerId(serverId);
                if (index >= 0) {
                    // We have this entity cached locally.  Update both the updated and original copies of this
                    // entity to reflect property values on "entity".

                    // From "entity", overwrite unmodified property values on our updated copy of this entity.
                    var oldOriginalEntity = self._originalEntities[index];
                    updatedEntity = self._updatedEntities[index];
                    self._mergeOntoUpdatedEntity(updatedEntity, entity, oldOriginalEntity, deferredEvents);

                    // "entity" becomes the original copy of this cached entity.
                    self._originalEntities[index] = entity;
                } else {
                    updatedEntity = $.extend({}, entity);  // TODO -- Only works for scalar-typed property values.

                    self._addAssociationProperties(updatedEntity);

                    var id = serverId.toString();  // Ok, since this is a new entity.
                    self._trackObservableForEntity(updatedEntity);
                    self._entityStates[id] = "Unmodified";

                    self._updatedEntities.push(updatedEntity);
                    self._originalEntities.push(entity);

                    entitiesNewToEntitySet.push(updatedEntity);
                }

                mergedLoadedEntities.push(updatedEntity);
            });

            if (entitiesNewToEntitySet.length > 0) {
                // Don't trigger a 'reset' here.  What would RemoteDataSources do with such an event?
                // They only have a subset of our entities as the entities they show their clients.  They could
                // only reapply their remote query in response to "refresh".
                var deferredEvent = MSD.Observability.insert(this._clientEntities, indexToInsertClientEntities, entitiesNewToEntitySet);
                deferredEvents.deferEvent(deferredEvent);
            }

            return mergedLoadedEntities;
        },

        __getEditedEntities: function () {
            var self = this,
            entities = [];
            $.each(this._entityStates, function (id, state) {
                if (state.indexOf("Client") === 0) {
                    entities.push(self._getEntityFromId(id));
                }
            });

            return entities;
        },

        __getEntityEdit: function (entity) {
            // TODO -- Throughout here, we should consult schema and strip off fields that aren't
            // compliant (like jQuery's __events__ field).

            var id = this.getEntityId(entity),
            self = this,
            submittingState,
            operation,
            index = this._getEntityIndexFromId(id),
            addEntityType = function (entityToExtend) {
                return $.extend({ "__type": self._entityType }, entityToExtend);
            };
            switch (this._entityStates[id]) {
                case "ClientUpdated":
                    submittingState = "ServerUpdating";
                    operation = {
                        Operation: 3,
                        Entity: addEntityType(this._updatedEntities[index]),
                        OriginalEntity: addEntityType(this._originalEntities[index])
                    };
                    break;

                case "ClientAdded":
                    submittingState = "ServerAdding";
                    var addedEntity = this._getAddedEntityFromId(id);
                    operation = {
                        Operation: 2,
                        Entity: addEntityType(addedEntity.entity)
                    };
                    break;

                case "ClientDeleted":
                    submittingState = "ServerDeleting";
                    operation = {
                        Operation: 4,
                        Entity: addEntityType(this._originalEntities[index])
                    };
                    // TODO -- Do we allow for concurrency guards here?
                    break;

                default:
                    throw "Unrecognized entity state.";
            }

            var edit = {
                updateEntityState: function (deferredEvents) {
                    self._updateEntityState(id, submittingState, deferredEvents);
                },
                operation: operation,
                succeeded: function (result, deferredEvents) {
                    return self._handleSubmitSucceeded(id, operation, result, deferredEvents);
                },
                failed: function (response, deferredEvents) {
                    self._handleSubmitFailed(id, operation, response, deferredEvents);
                }
            };
            return edit;
        },

        __revertChanges: function (deferredEvents) {
            var synchronizing;
            $.each(this._entityStates, function (unused, state) {
                if (state.indexOf("Server") === 0) {
                    synchronizing = true;
                    return false;
                }
            });
            if (synchronizing) {
                throw "Can't revert changes while a commit is in progress.";
            }

            var self = this;
            var uncommittedAddedEntities = $.grep(this._addedEntities, function (addedEntity) {
                return addedEntity.entity && $.inArray(addedEntity.entity, self._updatedEntities) < 0;
            });

            // Remove uncommitted added entities.
            if (uncommittedAddedEntities.length > 0) {
                this._addedEntities = $.grep(this._addedEntities, function (addedEntity) {
                    return $.inArray(addedEntity, uncommittedAddedEntities) < 0;
                });
                $.each(uncommittedAddedEntities, function (index, addedEntity) {
                    self._purgeUncommittedAddedEntity(addedEntity, true, deferredEvents);
                });
            }

            this._errors = [];

            // Revert "Client"-* entity states to "Unmodified" and discard changes to updatedEntities.
            for (var id in this._entityStates) {
                if (this._entityStates[id].indexOf("Client") === 0) {
                    this._revertToOriginalEntity(id, deferredEvents);
                    this._updateEntityState(id, "Unmodified", deferredEvents);
                }
            }
        },

        __handleEntitySetChanged: function (entityType, deferredEvents) {
            var fieldMetadata = (this._dataContext.__getMetadata(this._entityType) || {}).fields;
            if (!fieldMetadata) {
                return;
            }

            // Invalidate our child entities collections, in response to some change
            // to the target entity set.
            for (var id in this._childEntitiesCollections) {
                var childEntitiesCollections = this._childEntitiesCollections[id];
                var entity = this._getEntityFromId(id);
                for (var fieldName in childEntitiesCollections) {
                    var associationMetadata = fieldMetadata[fieldName];
                    if (associationMetadata.type === entityType) {
                        var childEntitiesCollection = childEntitiesCollections[fieldName];
                        var newChildEntities = this._computeAssociatedEntities(entity, associationMetadata);

                        // Perform adds/removes on childEntitiesCollection to have it reflect the same membership
                        // as newChildEntities.  Issue change events for the adds/removes.
                        // Don't try to preserve ordering between childEntitiesCollection and newChildEntities,
                        // so we don't, for instance, issue "move" events while we're in the midst of a client-issued
                        // add to a child entity collection.
                        // TODO -- Assert that we have only a single add or remove here.  My indices for events will
                        // be off for multiple adds/removes.
                        var addedEntities = $.grep(newChildEntities, function (childEntity) {
                            return $.inArray(childEntity, childEntitiesCollection) < 0;
                        });
                        $.each(addedEntities, function (unused, addedEntity) {
                            var deferredEvent = MSD.Observability.insert(childEntitiesCollection, childEntitiesCollection.length, [addedEntity]);
                            deferredEvents.deferEvent(deferredEvent);
                        });

                        var removedEntities = $.grep(childEntitiesCollection, function (childEntity) {
                            return $.inArray(childEntity, newChildEntities) < 0;
                        });
                        $.each(removedEntities, function (unused, removedEntity) {
                            var indexRemove = $.inArray(removedEntity, childEntitiesCollection),
                            deferredEvent = MSD.Observability.remove(childEntitiesCollection, indexRemove, 1);
                            deferredEvents.deferEvent(deferredEvent);
                        });

                        if (addedEntities.length > 0 || removedEntities.length > 0) {
                            // TODO -- Explicitly kick the data source ascribed to childEntitiesCollection to have it
                            // sync its internal cache (which we don't actually need) to childEntitiesCollection.
                            $.dataSource.unwrapHack(childEntitiesCollection).__syncToNewEntities();
                        }
                    }
                }
            }
        },

        __getEntityType: function () {
            return this._entityType;
        },

        // Private methods

        _handleCollectionChange: function (eventArguments) {
            switch (eventArguments.change) {
                case "insert":
                    // The value of eventArguments.index is not interesting at the EntitySet level.
                    // Ordering of RDS entities is established by the server.
                    // EntitySet collects both internal and external adds by appending them to its cache.

                    var entitiesToAdd = eventArguments.items;
                    if (entitiesToAdd.length > 1) {
                        throw "NYI -- Can only add a single entity to/from an array in one operation.";
                    }

                    var entityToAdd = entitiesToAdd[0],
                    self = this;
                    this._executeWithDeferredEvents(function (deferredEvents) {
                        self._addEntity(entityToAdd, deferredEvents);
                        // Assumes add to query result and not to collection-typed relationship property.
                    }, /* fromAddOrUpdate: */true);
                    // Re: fromAddOrUpdate, this will be triggered due to "arrayChanged" and we want a consistent view of our
                    // cache, regardless of "arrayChanged" ordering b/t us and clients.
                    break;

                case "remove":
                    var index = eventArguments.index;
                    var entitiesToDelete = eventArguments.items;
                    if (entitiesToDelete.length > 1) {
                        throw "NYI -- Can only remove a single entity to/from an array in one operation.";
                    }

                    // TODO -- Destructive deletes are currently NYI
                    //// this._deleteEntity(this.getEntityId(entitiesToDelete[0]));
                    throw "NYI -- Cannot apply destructive deletes to a entity collection.  Use 'deleteEntity' for non-destructive delete.";
                    break;

                default:
                    throw "NYI -- Array operation '" + eventArguments.change + "' is not supported.";
            }
        },

        _addEntity: function (entity, deferredEvents, skipCommit) {
            if ($.inArray(entity, this._updatedEntities) >= 0 ||  // ...in entities from last query
            this._getAddedEntityFromEntity(entity)) {  // ...in added entities
                throw "Entity already in data source.";
            }

            var id = "added" + (Math.floor(Math.random() * Math.pow(2, 32) + 1)).toString(),
            addedEntity = { entity: entity, clientId: id };
            this._addedEntities.push(addedEntity);
            // N.B.  Entity will already have been added to this._clientEntities, as clients issue CUD operations
            // against this._clientEntities.
            this._trackObservableForEntity(entity);
            this._entityStates[id] = "Unmodified";
            this._addAssociationProperties(entity);

            this._updateEntityState(id, "ClientAdded", deferredEvents);

            if (!skipCommit) {
                this._dataContext.__commitEntityIfImplicit(this, entity, deferredEvents);
            }

            deferredEvents.entitySetChanged(this._entityType);
        },

        _deleteEntity: function (entity) {
            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                var id = self.getEntityId(entity),
                index = self._getEntityIndexFromId(id),
                addedEntityBeingDeleted = self._getAddedEntityFromId(id),
                deletingAddedEntity = index < 0 && addedEntityBeingDeleted;
                if (deletingAddedEntity) {
                    var entityState = self._entityStates[id];
                    if (entityState === "ClientAdded") {
                        // Deleting a entity that is uncommitted and only on the client.
                        // TODO -- Reconsider whether we shouldn't throw here, force clients to revert instead.
                        self._purgeUncommittedAddedEntity(addedEntityBeingDeleted, true, deferredEvents);
                    } else {
                        // To be in addedEntities but not in updatedEntities, entity should either be in a
                        // pre-commit or committing state.
                        //// Assert(entityState === "ServerAdding");

                        // TODO -- Need to detect and enqueue dependent commits?
                        throw "NYI -- Can't edit a entity while previous edits are being committed.";
                    }
                } else if (index < 0) {
                    throw "Entity no longer cached in data source.";
                } else {
                    if (self._entityStates[id].indexOf("Server") === 0) {
                        // TODO -- Need to detect and enqueue dependent commits?
                        throw "NYI -- Can't edit a entity while previous edits are being committed.";
                    }

                    self._updateEntityState(id, "ClientDeleted", deferredEvents);

                    self._dataContext.__commitEntityIfImplicit(self, entity, deferredEvents);
                }
            });
        },

        _updateEntity: function (entity, deferredEvents) {
            var index = $.inArray(entity, this._updatedEntities),
            updatingAddedEntity = index < 0 && this._getAddedEntityFromEntity(entity);
            if (updatingAddedEntity) {
                var entityState = this._entityStates[this.getEntityId(entity)];
                if (entityState === "ClientAdded") {
                    // Updating a entity that is uncommitted and only on the client.
                    // Edit state remains "ClientAdded".  We won't event an edit state change (so clients had
                    // better be listening on "propertyChange").
                    // Fall through and do implicit commit, if appropriate.
                } else {
                    // To be in addedEntities but not in updatedEntities, entity should either be in a
                    // pre-commit or committing state.
                    //// Assert(entityState == "ServerAdding");

                    // TODO -- What do we do if this entity is in the process of being added to the server?
                    // We'll have to enqueue this update and treat it properly with respect to errors on the add, just
                    // as we'll enqueue any dependent edits?
                    throw "NYI -- Can't update an added entity while it's being committed.";
                }
            } else if (index < 0) {
                throw "Entity no longer cached in data source.";
            } else {
                var id = this.getEntityId(entity);

                if (this._entityStates[id].indexOf("Server") === 0) {
                    // TODO -- Need to detect and enqueue dependent commits?
                    throw "NYI -- Can't edit a entity while previous edits are being committed.";
                }

                // TODO -- What if entity is in the "ClientDeleted" state?  Should we discard the delete or throw?
                this._updateEntityState(id, "ClientUpdated", deferredEvents);
            }

            this._dataContext.__commitEntityIfImplicit(this, entity, deferredEvents);
        },

        _updateEntityState: function (id, state, deferredEvents, responseText, entity) {
            /// <param name="responseText" optional="true"></param>
            /// <param name="entity" optional="true"></param>

            var oldState = this._entityStates[id];
            if (this._entityStates[id]) {  // We'll purge the entity before raising "Deleted".
                this._entityStates[id] = state;
            }

            entity = entity || this._getEntityFromId(id);  // Notifying after a purge requires that we pass the entity for id.

            if (responseText) {
                var error = JSON.parse(responseText);  // TODO -- I've seen this in XML format too.
                this._errors.push({ entity: entity, error: error });
            }

            // TODO -- Use "error" to maintain lists of uncommitted and of in-error operations.
            // Allow for resolve/retry on in-error operations.

            if (oldState !== state) {
                var self = this;
                deferredEvents.deferEvent(function () {
                    self._raiseEntityStateChangedEvent(entity, state);
                });
            }
        },

        _purgeEntityAtIndex: function (indexToPurge, triggerArrayChange, deferredEvents) {
            var entityToPurge = this._updatedEntities[indexToPurge],
            idToPurge = this.getEntityId(entityToPurge);

            // Remove our observable extensions from the entity being purged.
            MSD.Observability.track(entityToPurge, null);

            this._updatedEntities = $.grep(this._updatedEntities, function (unused, index) {
                return index !== indexToPurge;
            });
            this._originalEntities = $.grep(this._originalEntities, function (unused, index) {
                return index !== indexToPurge;
            });
            this._purgeFromClientEntities(entityToPurge, triggerArrayChange, deferredEvents);

            delete this._entityStates[idToPurge];
            this._disposeChildEntitiesCollections(idToPurge);

            if (this._getAddedEntityFromId(idToPurge)) {
                this._addedEntities = $.grep(this._addedEntities, function (entity) { return entity.clientId !== idToPurge; });
            }

            this._errors = $.grep(this._errors, function (index, error) { return error.entity !== entityToPurge; });

            // TODO -- Have a specific event for entities leaving the cache?
        },

        _raisePropertyChangedEvent: function (entity, path, value) {
            this._raiseEvent("propertyChanged", entity, path, value);
        },

        _raiseEntityStateChangedEvent: function (entity, state) {
            this._raiseEvent("entityStateChanged", entity, state);
        },

        _raiseEvent: function (eventType) {
            var eventArguments = [].slice.call(arguments, 1),  // "arguments" isn't an Array.
            toNotify = this._observers.slice();
            $.each(toNotify, function (index, observer) {
                // TODO -- We should do a setTimeout for each callback, to keep the UI from hanging up.
                if ($.isFunction(observer[eventType])) {
                    observer[eventType].apply(null, eventArguments);
                }
            });
        },

        _getServerEntityId: function (entity) {
            return this._dataContext.__getMetadata(this._entityType).getServerId(entity);
        },

        _getEntityIndexFromId: function (id) {
            var addedEntity = this._getAddedEntityFromId(id),
            idToFind;
            if (!addedEntity) {
                idToFind = id;
            } else if (addedEntity.serverId === undefined) {
                return -1;
            } else {
                idToFind = addedEntity.serverId.toString();
            }

            var index = -1;
            for (var i = 0; i < this._originalEntities.length; i++) {
                // Compare based on string, since "id" is actually a client id.
                if (this._getServerEntityId(this._originalEntities[i]).toString() === idToFind) {
                    index = i;
                    break;
                }
            }

            return index;
        },

        // N.B.  Server ids are faithful to the native type of identity properties.  Clients ids are strings.
        _getEntityIndexFromServerId: function (id) {
            var index = -1;
            for (var i = 0; i < this._originalEntities.length; i++) {
                if (this._getServerEntityId(this._originalEntities[i]) === id) {
                    index = i;
                    break;
                }
            }

            return index;
        },

        _trackObservableForEntity: function (entity) {
            var self = this;
            MSD.Observability.track(entity, {
                afterChange: function (target, type, eventArguments) {
                    // Ensures that our handler is called ahead of any "arrayChange" event listeners,
                    // so the EntitySet can bring itself into a consistent state _before_ any clients are notified.
                    self._handlePropertyChange(target, eventArguments);
                },
                afterEvent: function (target, type, eventArguments) {
                    // Ensures that we issue our events _after_ all clients have been notified of internal adds.
                    self._flushDeferredEventsFromAddOrUpdate();
                }
            });
        },

        _handlePropertyChange: function (entity, eventArguments) {
            var path = eventArguments.path,
            value = eventArguments.value;
            // TODO: "path" here can actually be a dot-delimited path into a complex-typed property value.

            if (path === "") {
                // Data-linking sends all <input> changes to the linked object.
                return;
            }

            var id = this.getEntityId(entity);
            if (this._entityStates[id] === "Unmodified" &&
            entity[path] === this._originalEntities[this._getEntityIndexFromId(id)][path]) {
                // No-op
                return;
            }

            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                self._handlePropertyChangeInternal(entity, path, value, deferredEvents);
            }, /* fromAddOrUpdate: */true);
        },

        _handlePropertyChangeInternal: function (entity, path, value, deferredEvents) {
            // TODO: "path" here can actually be a dot-delimited path into a complex-typed property value.

            var self = this;
            deferredEvents.deferEvent(function () {
                self._raisePropertyChangedEvent(entity, path, value);
            });  // Issue the "entity change" event prior to any related "entity state changed" event below...
            this._updateEntity(entity, deferredEvents);  // TODO -- We need some property update facility.  This is whole-entity.

            deferredEvents.entitySetChanged(this._entityType);
        },

        // Updates "updatedEntity" (the updated version of this cached entity) based on like-named properties on
        // "sourceEntity".
        // Only locally modified properties on "updatedEntity" are changed, and "originalEntity" is used to determine
        // whether a given property has been modified locally.
        _mergeOntoUpdatedEntity: function (updatedEntity, sourceEntity, originalEntity, deferredEvents) {
            var self = this, changed;
            $.each(sourceEntity, function (key, value) {
                var isModifiedProperty = originalEntity && updatedEntity[key] !== originalEntity[key];
                if (!isModifiedProperty) {  // Only merge unmodified properties.
                    if (updatedEntity[key] !== value) {  // TODO -- Only works for scalar-typed property values.
                        changed = true;

                        var deferredEvent = MSD.Observability.setProperty(updatedEntity, key, value);
                        deferredEvents.deferEvent(deferredEvent);
                        deferredEvents.deferEvent(function () {
                            self._raisePropertyChangedEvent(updatedEntity, key, value);
                        });
                    }
                }
            });

            if (changed) {
                deferredEvents.entitySetChanged(this._entityType);
            }
        },

        _getAddedEntityFromId: function (id) {
            var addedEntities = $.grep(this._addedEntities, function (addedEntity) { return addedEntity.clientId === id; });
            // Assert(addedEntities.length <= 1);
            return addedEntities[0];
        },

        _getAddedEntityFromEntity: function (entity) {
            var addedEntities = $.grep(this._addedEntities, function (addedEntity) { return addedEntity.entity === entity; });
            // Assert(addedEntities.length <= 1);
            return addedEntities[0];
        },

        _handleSubmitSucceeded: function (id, operation, result, deferredEvents) {
            var success = true,
            entity = this._getEntityFromId(id);  // ...before we purge.

            switch (operation.Operation) {
                case 2:
                    if (result.ValidationErrors) {
                        success = false;
                        var self = this;
                        $.each(result.ValidationErrors, function (index, error) {
                            self._errors.push({ entity: entity, error: error });
                        });
                        this._updateEntityState(id, "ClientAdded", deferredEvents);
                    } else {
                        var newOriginalEntity = result.Entity;
                        //delete newOriginalEntity.__type;  // TODO: Review whether we should be relying on these.

                        var addedEntity = this._getAddedEntityFromId(id),
                        newUpdatedEntity = addedEntity.entity;

                        // Keep entity in addedEntities to maintain its synthetic id as the client-known id.
                        addedEntity.serverId = this._getServerEntityId(newOriginalEntity);

                        // Added entities will show up at the end of getEntities.  Clients need to refresh
                        // to reapply any ordering.
                        this._updatedEntities.push(newUpdatedEntity);
                        this._originalEntities.push(newOriginalEntity);

                        // Merge after adding to updated/originalEntities, since we'll issue callbacks during merge.
                        this._revertToOriginalEntity(addedEntity.clientId, deferredEvents);

                        this._updateEntityState(id, "Unmodified", deferredEvents);
                    }
                    break;

                case 3:
                    // TODO: The edit failed if operation.ConflictMembers is non-empty
                    var updatedOriginalEntity = result.Entity;
                    //delete updatedOriginalEntity.__type;  // TODO: Review whether we should be relying on these.
                    this._originalEntities[this._getEntityIndexFromId(id)] = updatedOriginalEntity;
                    this._revertToOriginalEntity(id, deferredEvents);
                    this._updateEntityState(id, "Unmodified", deferredEvents);
                    break;

                case 4:
                    // TODO: The edit failed if operation.IsDeleteConflict.
                    var indexToPurge = this._getEntityIndexFromId(id);
                    this._purgeEntityAtIndex(indexToPurge, true, deferredEvents);
                    this._updateEntityState(id, "Deleted", deferredEvents, null, entity);
                    break;
            }

            if (success) {
                this._errors = $.grep(this._errors, function (index, error) { return error.entity !== entity; });
            }

            return success;
        },

        _handleSubmitFailed: function (id, operation, response, deferredEvents) {
            var state;
            switch (operation.Operation) {
                case 2: state = "ClientAdded"; break;
                case 3: state = "ClientUpdated"; break;
                case 4: state = "ClientDeleted"; break;
            }
            this._updateEntityState(id, state, deferredEvents, response.responseText);
        },

        _revertToOriginalEntity: function (id, deferredEvents) {
            var index = this._getEntityIndexFromId(id);
            this._mergeOntoUpdatedEntity(this._updatedEntities[index], this._originalEntities[index], null, deferredEvents);
        },

        _purgeUncommittedAddedEntity: function (addedEntityBeingPurged, triggerArrayChange, deferredEvents) {
            var id = addedEntityBeingPurged.clientId,
            entity = addedEntityBeingPurged.entity;
            // Remove our observable extensions from the entity being purged.
            MSD.Observability.track(entity, null);
            this._addedEntities = $.grep(this._addedEntities, function (addedEntity) { return addedEntity !== addedEntityBeingPurged; });
            delete this._entityStates[id];
            this._disposeChildEntitiesCollections(id);
            this._errors = $.grep(this._errors, function (index, error) { return error.entity !== entity; });
            this._purgeFromClientEntities(entity, triggerArrayChange, deferredEvents);
            this._updateEntityState(id, "Deleted", deferredEvents, null, entity);
        },

        _purgeFromClientEntities: function (entity, triggerArrayChange, deferredEvents) {
            var index = $.inArray(entity, this._clientEntities),
            deferredEvent = MSD.Observability.remove(this._clientEntities, index, 1);
            if (triggerArrayChange) {
                deferredEvents.deferEvent(deferredEvent);
            }

            deferredEvents.entitySetChanged(this._entityType);
        },

        _getEntityFromId: function (id) {
            var self = this;
            return $.grep(this.getEntities(), function (entity) { return self.getEntityId(entity) === id; })[0];
        },

        // TODO -- This code is largely duplicated in DataContext.js.
        _executeWithDeferredEvents: function (toExecute, fromAddOrUpdate) {
            /// <param name="fromAddOrUpdate" optional="true"></param>

            this._assertNotFlushingDeferredEvents();

            var deferredEvents = [],
            entitySetChanged,
            deferredEventsCollector = {
                deferEvent: function (eventToDefer) {
                    if (eventToDefer) {
                        deferredEvents.push(eventToDefer);
                    }
                },
                entitySetChanged: function (entityType) {
                    // TODO -- Assert entityType === this._entityType.
                    entitySetChanged = true;
                }
            },
            result = toExecute(deferredEventsCollector);
            if (entitySetChanged) {
                this._dataContext.__entitySetChanged(this._entityType, deferredEventsCollector);
            }

            if (fromAddOrUpdate) {
                // We defer events related to internal adds or updates so that all clients receive
                // their arrayChange/propertyChange callbacks before they receive our callbacks.
                // Delete doesn't get such treatment because for non-destructive deletes there is
                // no "arrayChange" event accompanying the internal delete.
                var self = this;
                $.each(deferredEvents, function (index, deferredEvent) {
                    self._deferredEvents.push(deferredEvent);
                });
            } else {
                this._flushDeferredEvents(deferredEvents);
            }

            return result;
        },

        _flushDeferredEventsFromAddOrUpdate: function () {
            if (this._deferredEvents.length !== 0) {
                var deferredEvents = this._deferredEvents;
                this._deferredEvents = [];
                this._flushDeferredEvents(deferredEvents);
            }
        },

        _flushDeferredEvents: function (deferredEvents) {
            try {
                this._flushingDeferredEvents = true;
                $.each(deferredEvents, function (index, deferredEvent) {
                    deferredEvent();
                });
            } finally {
                this._flushingDeferredEvents = false;
            }
        },

        _assertNotFlushingDeferredEvents: function () {
            if (this._flushingDeferredEvents) {
                // This catches bugs in our implementation where we try to do event deferral
                // in the process of issuing event callbacks.
                // It would also be triggered when a client tries to make a side-effecting API
                // call while we're flushing our deferred events.
                // TODO -- We should strengthen this further to catch more cases where the client
                // makes side-effecting API calls during event callbacks.  This only catches the
                // offenses during deferred event callbacks.
                throw "Issuing side-effecting operations during event callbacks is not supported.";
            }
        },

        _addAssociationProperties: function (entity) {
            var fieldsMetadata = (this._dataContext.__getMetadata(this._entityType) || {}).fields;
            if (fieldsMetadata) {
                var self = this;
                $.each(fieldsMetadata, function (fieldName, fieldMetadata) {
                    if (fieldMetadata.association) {
                        if (fieldMetadata.association.isForeignKey) {
                            entity["get_" + fieldName] = function () {
                                return self._getParentEntity(entity, fieldName);
                            };
                            entity["set_" + fieldName] = function (parentEntity) {
                                return self._setParentEntity(entity, fieldName, parentEntity);
                            };
                        } else if (fieldMetadata.array) {
                            entity["get_" + fieldName] = function () {
                                return self._getChildEntities(entity, fieldName);
                            };
                        } else {
                            // TODO -- Singleton child entities?
                        }
                    }
                });
            }
        },

        _getParentEntity: function (entity, fieldName) {
            var associationMetadata = this._dataContext.__getMetadata(this._entityType).fields[fieldName];  // Will be non-null, if we get here.
            var parentEntity = this._computeAssociatedEntities(entity, associationMetadata)[0];
            return parentEntity || null;
        },

        _setParentEntity: function (entity, fieldName, parentEntity) {
            var associationMetadata = this._dataContext.__getMetadata(this._entityType).fields[fieldName];  // Will be non-null, if we get here.

            var targetEntitySet = this._dataContext.getEntitySet(associationMetadata.type);
            if ($.inArray(parentEntity, targetEntitySet.getEntities()) < 0) {
                // TODO -- Should this implicitly add the parent entity?  I doubt it.
                throw "Parent entity is not in the parent entity set for this association.";
            } else if ((targetEntitySet.getEntityState(parentEntity) || "").indexOf("Add") > 0) {
                // TODO -- Add support for added parent entities without an established key value, fix-up after commit.
                throw "NYI -- Cannot set foreign keys to key values computed from added entities.  Commit your parent entity first.";
            }

            var targetKey = associationMetadata.association.otherKey,
            targetKeyValue = parentEntity ? parentEntity[targetKey[0]] : null;  // TODO -- Generalize to N fields.
            if (targetKeyValue === undefined) {
                throw "Parent entity has no value for its '" + targetKey[0] + "' key property.";
            }

            var sourceKey = associationMetadata.association.thisKey,
            sourceKeyValue = entity[sourceKey[0]],  // TODO -- Generalize to N fields.
            setForeignKeyValue;
            if (!parentEntity) {
                if (sourceKeyValue !== null) {
                    setForeignKeyValue = true;
                }
            } else if (sourceKeyValue === undefined || sourceKeyValue !== targetKeyValue) {
                setForeignKeyValue = true;
            }

            if (setForeignKeyValue) {
                var self = this;
                this._executeWithDeferredEvents(function (deferredEvents) {
                    var deferredEvent = MSD.Observability.setProperty(entity, sourceKey[0], targetKeyValue);  // TODO -- Generalize to N fields.
                    deferredEvents.deferEvent(deferredEvent);
                    self._handlePropertyChangeInternal(entity, sourceKey[0], targetKeyValue, deferredEvents);
                });
                // TODO -- Should we trigger "propertyChange" on the child entity's parent property here?
            }
        },

        _getChildEntities: function (entity, fieldName) {
            var id = this.getEntityId(entity),
            childEntitiesCollections = this._childEntitiesCollections[id];
            if (!childEntitiesCollections) {
                childEntitiesCollections = this._childEntitiesCollections[id] = {};
            }

            var childEntitiesCollection = childEntitiesCollections[fieldName];
            if (!childEntitiesCollection) {
                var associationMetadata = this._dataContext.__getMetadata(this._entityType).fields[fieldName];  // Will be non-null, if we get here.

                childEntitiesCollection = this._computeAssociatedEntities(entity, associationMetadata);

                var self = this;

                var handleAddEntity = function (entityToAdd) {
                    if ((self.getEntityState(entity) || "").indexOf("Add") > 0) {
                        // TODO -- Add support for added parent entities without an established key value, fix-up after commit.
                        throw "NYI -- Cannot set foreign keys to key values computed from added entities.  Commit the parent entity first.";
                    }

                    var sourceKey = associationMetadata.association.thisKey,
                    sourceKeyValue = entity[sourceKey[0]];  // TODO -- Generalize to N fields.
                    if (sourceKeyValue === undefined) {
                        throw "Parent entity has no value for its '" + sourceKey[0] + "' key property.";
                    }

                    var targetKey = associationMetadata.association.otherKey,
                    targetEntitySet = self._dataContext.getEntitySet(associationMetadata.type);
                    targetEntitySet._handleAddToChildEntitiesCollection(entityToAdd, targetKey, sourceKeyValue, function () {
                        $.dataSource.unwrapHack(childEntitiesCollection).__syncToNewEntities();
                    });
                };

                var flushDeferredEventsFromAdd = function () {
                    var targetEntitySet = self._dataContext.getEntitySet(associationMetadata.type);
                    targetEntitySet._flushDeferredEventsFromAddOrUpdate();
                };

                var dataSource = new MSD.AssociatedEntitiesDataSource(
                    this._dataContext, associationMetadata.type, childEntitiesCollection, handleAddEntity, flushDeferredEventsFromAdd);
                $.dataSource.wrapHack(childEntitiesCollection, dataSource);  // TODO -- Rework factoring.

                childEntitiesCollections[fieldName] = childEntitiesCollection;
            }

            return childEntitiesCollection;
        },

        _computeAssociatedEntities: function (entity, associationMetadata) {
            var sourceKeyValue = entity[associationMetadata.association.thisKey[0]];  // TODO -- Generalize to N fields.
            var targetEntitySet = this._dataContext.getEntitySet(associationMetadata.type);
            var targetEntities = targetEntitySet._getTargetEntities(associationMetadata.association.otherKey, sourceKeyValue);
            return targetEntities;
        },

        _getTargetEntities: function (key, keyValue) {
            var targetEntities = $.grep(this._clientEntities, function (entity) {
                var targetKeyValue = entity[key[0]];  // TODO -- Generalize to N fields.
                return targetKeyValue !== undefined && targetKeyValue === keyValue;
                // TODO -- Confirm correct equivalence check here.
            });
            return targetEntities;
        },

        _disposeChildEntitiesCollections: function (id) {
            var childEntitiesCollections = this._childEntitiesCollections[id];
            if (childEntitiesCollections) {
                $.each(childEntitiesCollections, function (unused, childEntitiesCollection) {
                    $([childEntitiesCollection]).dataSource().destroy();
                });
            }
            delete this._childEntitiesCollections[id];
        },

        _handleAddToChildEntitiesCollection: function (entityToAdd, targetKey, sourceKeyValue, syncChildEntitiesCollectionDataSource) {
            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                var targetKeyValue = entityToAdd[targetKey[0]],  // TODO -- Generalize to N fields.
                setForeignKeyValue = targetKeyValue === undefined || targetKeyValue !== sourceKeyValue;
                // TODO -- Add support for added parent entities without an established key value, fix-up after commit.

                var childEntitiesCollectionChanged;
                if ($.inArray(entityToAdd, self.getEntities()) < 0) {
                    // Perform our insert via an internal protocol and not via $.observable().insert so that all clients 
                    // of the child entity collection receive "entityStateChanged" _after_ "arrayChange" events.  

                    // _addEntity itself doesn't update _clientEntities nor does it trigger "arrayChange", since it's meant
                    // to react to "arrayChange" from internal adds by the client.
                    deferredEvents.deferEvent(MSD.Observability.insert(self._clientEntities, self._clientEntities.length, [entityToAdd]));

                    var skipCommit = setForeignKeyValue;  // In implicit commit mode, we want to commit once, during setForeignKeyValue below.
                    self._addEntity(entityToAdd, deferredEvents, skipCommit);

                    childEntitiesCollectionChanged = true;
                }

                if (setForeignKeyValue) {
                    // Do this after the entitySet add above.  That way, the property change will be observable by clients
                    // interested in childEntitiesCollection or the EntitySet.
                    // Likewise, above, we will have done MSD.Observability.track (as part of adding to the EntitySet) before 
                    // MSD.Observability.setProperty, in case establishing observable proxies is done implicitly w/in setProperty
                    // (as WinJS support does).

                    deferredEvents.deferEvent(MSD.Observability.setProperty(entityToAdd, targetKey[0], sourceKeyValue));  // TODO -- Generalize to N fields.
                    self._handlePropertyChangeInternal(entityToAdd, targetKey[0], sourceKeyValue, deferredEvents);

                    childEntitiesCollectionChanged = true;
                }

                if (childEntitiesCollectionChanged) {
                    // TODO -- Explicitly kick the data source ascribed to childEntitiesCollection to have it
                    // sync its internal cache (which we don't actually need) to childEntitiesCollection.
                    syncChildEntitiesCollectionDataSource();
                }
            }, /* fromAddOrUpdate: */true);
            // Re: fromAddOrUpdate, this will be triggered due to "arrayChanged" on a child entity collection and 
            // we want a consistent view of our cache, regardless of "arrayChanged" ordering b/t the entity set and
            // clients.
        }
    };

    MSD.EntitySet = MSD.defineClass(ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// <reference path="EntitySet.js" />
(function (global, MSD, undefined) {

    var ctor = function (serviceUrl, format, bufferChanges) {

        this._serviceUrl = serviceUrl;
        this._changeTracking = !!bufferChanges;

        var dataProvider;
        if (format) {
            var createProvider = MSD[format + "DataProvider"];
            if (!createProvider) {
                throw "There is no provider available for the format '" + format + "'";
            }
            dataProvider = new createProvider(serviceUrl);
        } else {
            dataProvider = new MSD.DataProvider(serviceUrl);
        }
        this._dataProvider = dataProvider;

        this._observers = [];
        this._entitySets = {};
        this._flushingDeferredEvents = false;
        this._metadata = {};        
    };

    var instanceMembers = {

        // Public methods

        dispose: function () {
            // TODO -- Do we want a dispose protocol to unbind our EntitySet "arrayChange" handlers?
        },

        // TODO -- We should align with jQuery binding/events here.
        addObserver: function (observer) {
            /// <summary>Adds an observer for one or more of the following events: commitStart, commit</summary>
            /// <param name="observer" type="Object">The observer to add</param>
            if ($.inArray(observer, this._observers) < 0) {
                this._observers.push(observer);
            }
        },

        removeObserver: function (observer) {
            /// <summary>Removes the specified observer</summary>
            /// <param name="observer" type="Object">The observer to remove</param>
            this._observers =
            $.grep(this._observers, function (element, index) {
                return element !== observer;
            });
        },

        getEntitySet: function (entityType) {
            var entitySet = this._entitySets[entityType];
            if (!entitySet) {
                entitySet = this._entitySets[entityType] = new MSD.EntitySet(this, entityType);
            }
            return entitySet;
        },

        getErrors: function () {
            var errors = [];
            $.each(this._entitySets, function (type, entitySet) {
                var spliceArguments = [errors.length, 0].concat(entitySet.getErrors());
                [ ].splice.apply(errors, spliceArguments);
            });
            return errors;
        },

        commitChanges: function () {
            if (!this._changeTracking) {
                throw "Data context must be in change-tracking mode to explicitly commit changes.";
            }

            var editedEntities = [];
            $.each(this._entitySets, function (type, entitySet) {
                var editedEntitiesT = $.map(entitySet.__getEditedEntities(), function (entity) {
                    return { entitySet: entitySet, entity: entity };
                });
                var spliceArguments = [editedEntities.length, 0].concat(editedEntitiesT);
                [ ].splice.apply(editedEntities, spliceArguments);
            });

            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                self._submitChanges(editedEntities, deferredEvents);
            });
        },

        revertChanges: function () {
            var self = this;
            this._executeWithDeferredEvents(function (deferredEvents) {
                $.each(self._entitySets, function (type, entitySet) {
                    entitySet.__revertChanges(deferredEvents);
                });
            });
        },

        merge: function (entities, type, includedEntities) {
            /// <summary>Merges data into the cache</summary>
            /// <param name="entities" type="Array">The array of entities to add or merge into the cache</param>
            /// <param name="type" type="String">The type of the entities to be merge into the cache. This parameter can be null/undefined when no entities are supplied</param>
            /// <param name="includedEntities" type="Array">An additional array of entities (possibly related) to add or merge into the cache.  These entities will not be returned from this function. This parameter is optional</param>
            /// <returns type="Array">The array of entities with newly merged values</returns>

            var self = this,
                mergedEntities;
            this._executeWithDeferredEvents(function (deferredEvents) {
                if (includedEntities) {
                    $.each(includedEntities, function (type, entities) {
                        // TODO -- Add CT + shredding support
                        var entitySet = self.getEntitySet(type);
                        entitySet.__loadEntities(entities, deferredEvents);
                    });
                }
                var entitySet = type && self.getEntitySet(type);
                mergedEntities = entitySet ? entitySet.__loadEntities(entities, deferredEvents) : [];
            });

            return mergedEntities;
        },

        addMetadata: function (type, metadata) {
            /// <summary>Adds metadata describing a given type.  This must be called before data of the type is first merged into this DataContext</summary>
            /// <param name="type" type="String">The type for which metadata is being added</param>
            /// <param name="metadata" type="Object">The metadata that is being added</param>
            if (!this._metadata[type]) {
                var entitySet = this._entitySets[type];
                if (entitySet && entitySet.getEntities().length) {
                    throw "Cannot add metadata for type once data of that type has been merged into this DataContext.";
                }

                this._metadata[type] = metadata;
            }
            // TODO: ...else validate that this._metadata[type] == metadata
        },

        // TODO -- We have no mechanism to similarly clear data sources.
        //// clear: function () {
        ////     $.each(this._entitySets, function (type, entitySet) {
        ////         entitySet.__clear();
        ////     });
        //// },

        // Internal methods

        __load: function (query, success) {
            $.each(this._entitySets, function (type, entitySet) {
                if (entitySet.__hasUncommittedEdits()) {
                    throw "Load is not allowed while the data source contains uncommitted edits.";
                }
            });

            var dataProvider = this._dataProvider,
                self = this;
            dataProvider
                .get(query.queryName, query.queryParameters, query)
                .done(function (result) {
                    result = dataProvider.load(result, self);
                    success(self.getEntitySet(result.type), result.entities, result.totalCount);
                });
            // TODO -- Need failure callback here.  How does it affect events/callbacks?
        },

        __commitEntityIfImplicit: function (entitySet, entity, deferredEvents) {
            if (!this._changeTracking) {
                this._submitChanges([{ entitySet: entitySet, entity: entity}], deferredEvents);
            }
        },

        __entitySetChanged: function (entityType, deferredEventsCollector) {
            this._distributeEntitySetChanged(entityType, deferredEventsCollector);
        },

        __getMetadata: function (entityType) {
            return this._metadata[entityType];
        },

        // Private methods

        _raiseCommitEvent: function () {
            this._raiseEvent("commit");
        },

        _raiseCommittingEvent: function () {
            this._raiseEvent("commitStart");
        },

        _raiseEvent: function (eventType) {
            var eventArguments = [].slice.call(arguments, 1),  // "arguments" isn't an Array.
            toNotify = this._observers.slice();
            $.each(toNotify, function (index, observer) {
                // TODO -- We should do a setTimeout for each callback, to keep the UI from hanging up.
                if ($.isFunction(observer[eventType])) {
                    observer[eventType].apply(null, eventArguments);
                }
            });
        },

        _submitChanges: function (editedEntities, deferredEvents) {

            this._raiseCommittingEvent();

            var edits = $.map(editedEntities, function (editedEntity) {
                return editedEntity.entitySet.__getEntityEdit(editedEntity.entity);
            });

            $.each(edits, function (index, edit) { edit.updateEntityState(deferredEvents); });

            var operations = $.map(edits, function (edit, index) {
                return $.extend({ Id: index.toString() }, edit.operation);
            });

            var self = this,
            success = function (submitResult) {
                var hasErrors = self._executeWithDeferredEvents(function (deferredEvents) {
                    for (var i = 0; i < submitResult.length; i++) {
                        var edit = edits[i];
                        if (!edit.succeeded(submitResult[i], deferredEvents)) {
                            return true;
                        }
                    }
                    return false;
                });
                if (!hasErrors) {
                    self._raiseCommitEvent();
                }
            },
            error = function (response) {
                self._executeWithDeferredEvents(function (deferredEvents) {
                    $.each(edits, function (index, edit) {
                        edit.failed(response, deferredEvents);
                    });
                });
            };
            this._dataProvider.post("SubmitChanges", { changeSet: operations }).then(success, error);
        },

        _executeWithDeferredEvents: function (toExecute) {
            this._assertNotFlushingDeferredEvents();

            var deferredEvents = [],
            changedEntitySets = {},
            deferredEventsCollector = {
                deferEvent: function (eventToDefer) {
                    if (eventToDefer) {
                        deferredEvents.push(eventToDefer);
                    }
                },
                entitySetChanged: function (entityType) {
                    changedEntitySets[entityType] = true;
                }
            },
            result = toExecute(deferredEventsCollector);
            for (var changedEntitySetType in changedEntitySets) {
                this._distributeEntitySetChanged(changedEntitySetType, deferredEventsCollector);
            }

            try {
                this._flushingDeferredEvents = true;
                $.each(deferredEvents, function (index, deferredEvent) {
                    deferredEvent();
                });
            } finally {
                this._flushingDeferredEvents = false;
            }

            return result;
        },

        _distributeEntitySetChanged: function (changedEntitySetType, deferredEvents) {
            // Give entity sets an opportunity to invalidate associated entity collections for
            // their entities.
            for (var entityType in this._entitySets) {
                var entitySetToNotify = this._entitySets[entityType];
                entitySetToNotify.__handleEntitySetChanged(changedEntitySetType, deferredEvents);
            }
        },

        _assertNotFlushingDeferredEvents: function () {
            if (this._flushingDeferredEvents) {
                // This catches bugs in our implementation where we try to do event deferral
                // in the process of issuing event callbacks.
                // It would also be triggered when a client tries to make a side-effecting API
                // call while we're flushing our deferred events.
                // TODO -- We should strengthen this further to catch more cases where the client
                // makes side-effecting API calls during event callbacks.  This only catches the
                // offenses during deferred event callbacks.
                throw "Issuing side-effecting operations during event callbacks is not supported.";
            }
        }
    };

    MSD.DataContext = MSD.defineClass(ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
(function (global, MSD, undefined) {

    var ctor = function (serviceUrl) {

        this._serviceUrl = serviceUrl;
    };

    function transformQuery(query) {
        var queryParameters = {};

        // $where/where/filters -> $where
        if (query.$where || query.where) {
            queryParameters.$where = query.$where || query.where;
        } else if (query.filters) {
            var whereParameter = "",
                applyOperator = function (property, value, operator) {
                    if (typeof value === "string") {
                        value = '"' + value + '"';
                    }

                    switch (operator) {
                        case "<":
                        case "<=":
                        case "==":
                        case "!=":
                        case ">=":
                        case ">": return property + operator + value;
                        case "StartsWith":
                        case "EndsWith":
                        case "Contains": return property + "." + operator + "(" + value + ")";
                        default: throw "The operator '" + operator + "' is not supported.";
                    }
                };

            $.each(query.filters, function (index, filter) {
                if (index > 0) {
                    whereParameter += " AND ";
                }
                if (filter.filterValue === undefined) {
                    whereParameter += filter.filterProperty;
                } else {
                    whereParameter += applyOperator(filter.filterProperty, filter.filterValue, filter.filterOperator || "==");
                }
            });

            queryParameters.$where = whereParameter;
        }

        // $orderby/orderby/sort -> $orderby
        if (query.$orderby || query.orderby) {
            queryParameters.$orderby = query.$orderby || query.orderby;
        } else if (query.sort) {
            var orderbyParameter = "",
            formatSort = function (sort) {
                return sort.property + (sort.direction ? " " + sort.direction : "");
            };

            if (Object.prototype.toString.call(query.sort) === "[object String]") {
                orderbyParameter = query.sort;
            } else if (Object.prototype.toString.call(query.sort) === "[object Array]") {
                $.each(query.sort, function (index, sort) {
                    if (index > 0) {
                        orderbyParameter += ",";
                    }
                    orderbyParameter += formatSort(sort);
                });
            } else {
                orderbyParameter = formatSort(query.sort);
            }

            queryParameters.$orderby = orderbyParameter;
        }

        // $skip/skip -> $skip
        if (query.$skip || query.skip) {
            queryParameters.$skip = query.$skip || query.skip;
        }

        // $take/take -> $take
        if (query.$take || query.take) {
            queryParameters.$take = query.$take || query.take;
        }

        // $includeTotalCount/includeTotalCount -> $includeTotalCount
        if (query.$includeTotalCount || query.includeTotalCount) {
            queryParameters.$includeTotalCount = query.$includeTotalCount || query.includeTotalCount;
        }

        return queryParameters;
    }

    function transformParameters(parameters) {
        // perform any required transformations on the specified parameters
        // before invoking the service, for example json serializing arrays
        // and other complex parameters.
        if (parameters) {
            $.each(parameters || {}, function (key, value) {
                if ($.isArray(value)) {
                    // json serialize arrays since this is the format the json
                    // endpoint expects.
                    parameters[key] = JSON.stringify(value);
                }
            });
        }

        return parameters;
    }

    var instanceMembers = {

        // Public methods

        get: function (operation, parameters, queryParameters) {
            /// <summary>
            /// Asynchronously gets data from the server using the specified operation, parameters, and query
            /// </summary>
            /// <param name="operation" type="String">The operation name</param>
            /// <param name="parameters" type="Object">An object where each property is a parameter to pass to the operation. This parameter is optional.</param>
            /// <param name="queryParameters" type="Object">An object where each property is a query to pass to the operation. This parameter is optional.</param>
            /// <returns type="Promise">A Promise representing the result of the load operation</returns>

            /* Handle a service base URL with or without a trailing slash */
            var slash = (this._serviceUrl.substring(this._serviceUrl.length - 1) !== "/" ? "/" : "");

            // Invoke the query
            return $.ajax({
                url: this._serviceUrl + slash + "json/" + operation,
                data: $.extend({}, transformParameters(parameters), transformQuery(queryParameters || {})),
                dataType: "json"
            });
            // TODO: For now, the result we'll eventually return is in the native protocol form and thus should be 
            // treated as opaque to all but the "load" function below.
        },

        load: function (getResult, dataContext) {
            /// <summary>
            /// Loads data from a previous "get" call into the supplied DataContext
            /// </summary>
            /// <param name="getResult" type="Object">The result from some previous invocation of "get" on this DataProvider</param>
            /// <param name="dataContext" type="DataContext">The DataContext into which the result should be loaded</param>
            /// <returns type="Array">An array containing entities merged into the DataContext</returns>

            // Determine what key our result is under.  This saves us polluting the API surface
            // by passing the operation explicitly.
            var resultKey;
            $.each(getResult, function (key) {
                if (/Result$/.test(key)) {
                    resultKey = key;
                    return false;
                }
            });
            var result = getResult[resultKey];

            $.each(result.Metadata, function (unused, metadataForType) {
                dataContext.addMetadata(metadataForType.type, {
                    getServerId: function (entity) {
                        return entity[metadataForType.key[0]];
                    },

                    fields: metadataForType.fields,
                    rules: metadataForType.rules,
                    messages: metadataForType.messages
                });
            });

            $.each(result.RootResults, function (unused, entity) {
                delete entity.__type;
            });

            var includedEntities = {};
            if (result.IncludedResults) {
                $.each(result.IncludedResults, function (unused, entity) {
                    var entityType = entity.__type;
                    delete entity.__type;

                    var entities = includedEntities[entityType] || (includedEntities[entityType] = []);
                    entities.push(entity);
                });
            }

            var resultType = result.Metadata[0].type,
                mergedEntities = dataContext.merge(result.RootResults, resultType, includedEntities);

            return {
                entities: mergedEntities,
                type: resultType,
                totalCount: result.TotalCount || 0
                // For some reason DomainService sends no TotalCount for some queries when there are
                // no queryResult.RootResults.
            };
        },

        post: function (operation, parameters) {
            /// <summary>
            /// Asynchronously invokes the operation using the specified parameters
            /// </summary>
            /// <param name="operation" type="String">The operation name</param>
            /// <param name="parameters" type="Object">An object where each property is a parameter to pass to the operation. This parameter is optional.</param>
            /// <returns type="Promise">A Promise representing the result of the post operation</returns>

            var deferred = $.Deferred(),
                /* Handle a service base URL with or without a trailing slash */
                slash = (this._serviceUrl.substring(this._serviceUrl.length - 1) !== "/" ? "/" : ""),
                encodedParameters = JSON.stringify(parameters);

            // Invoke the query
            $.ajax({
                url: this._serviceUrl + slash + "json/" + operation,
                contentType: "application/json",
                data: encodedParameters,
                dataType: "json",
                type: "POST",
                success: function (data) {
                    var result = data[operation + "Result"];
                    $.each(result, function (unused, resultItem) {
                        if (resultItem.Entity) {
                            delete resultItem.Entity.__type;
                        }
                    });
                    deferred.resolve(result);
                    // TODO - this may actually be the error reporting case as well...
                },
                error: function (response) {
                    deferred.reject(response);
                }
            });

            return deferred.promise();
        }
    }

    MSD.DataProvider = MSD.defineClass(ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// <reference path="DataSource.js" />
/// <reference path="DataContext.js" />
/// <reference path="EntitySet.js" />
(function (global, MSD, undefined) {

    var ctor = function (serviceUrl, queryName, entityType, options) {

        this._entitySet = null;

        // Optional query options
        this._sort = null;
        this._filters = null;

        var format, queryParameters, bufferChanges, dataContext;
        if (options) {
            format = options.format;
            queryParameters = options.queryParameters;
            bufferChanges = options.bufferChanges;
            dataContext = options.dataContext;

            // Should be in DataSource, but AssociatedEntitiesDataSource supplies non-empty entityCollection.
            if (!dataContext && options.entityCollection && options.entityCollection.length !== 0) {
                throw "NYI -- Currently, entity collection must be empty to bind to a data source.";
            }
        }

        this._queryName = queryName;
        this._queryParameters = queryParameters;

        if (!dataContext) {
            dataContext = new MSD.DataContext(serviceUrl, format, !!bufferChanges);
            // TODO -- If DS exclusively owns the DC, can we make it non-accumulating?
        }

        this._dataContext = dataContext;
        var self = this;
        this._dataContextObserver = {
            commitStart: function () { self._onCommitStart(); },
            commit: function () { self._onCommit(); }
        };
        this._dataContext.addObserver(this._dataContextObserver);

        this._entitySetObserver = {
            propertyChanged: function (entity, property, newValue) { self._onPropertyChanged(entity, property, newValue); },
            entityStateChanged: function (entity, state) { self._onEntityStateChanged(entity, state); }
        };
        if (entityType) {
            // If clients supply a entity type, then they'll be able to do entity creation
            // without loading the underlying data source first.
            ////TODO: fix this
            //var entitySet = dataContext.getEntitySet(entityType);
            //this._bindToEntitySet(entitySet);
        }

        var inputData = {
            getEntities: function () { return self._entitySet.getEntities(); },
            getEntityState: function (entity) { return self._entitySet.getEntityState(entity); },  // TODO: We should screen "entity" to ensure it's in our array (throughout).
            getEntityValidationRules: function () {
                return {
                    rules: self._dataContext.__getMetadata(self._getEntityType()).rules,
                    messages: self._dataContext.__getMetadata(self._getEntityType()).messages
                };
            },
            isPropertyChanged: function (entity, propertyName) { return self._entitySet.isPropertyChanged(entity, propertyName); },
            getErrors: function () { return self._dataContext.getErrors(); },
            revertChange: function (entity, propertyName) { return self._entitySet.revertChange(entity, propertyName); },
            revertChanges: function (all) {  // TODO -- Not "all" is weird.  Remove.
                if (!!all) {
                    self._dataContext.revertChanges();
                } else {
                    self._entitySet.revertChanges();
                }
            },
            getEntityId: function (entity) { return self._entitySet.getEntityId(entity); }
        };

        this._super.constructor.call(this, inputData, options);
    };

    var instanceMembers = {

        // Public methods

        dispose: function () {
            MSD.DataSource.prototype.dispose.apply(this);
            if (this._dataContextObserver) {
                this._dataContext.removeObserver(this._dataContextObserver);
                this._dataContextObserver = null;
            }
            if (this._entitySetObserver && this._entitySet) {
                this._entitySet.removeObserver(this._entitySetObserver);
                this._entitySetObserver = null;
            }
        },

        getDataContext: function () {
            return this._dataContext;
        },

        setSort: function (sort) {
            // TODO -- Validate sort specification?
            this._sort = sort;
        },

        setFilter: function (filter) {
            if (!filter) {
                this._filters = null;  // Passing null/undefined means clear filter.
            } else if (Object.prototype.toString.call(filter) === "[object Array]") {
                var self = this;
                this._filters = $.map(filter, function (filterPart) {
                    return self._processFilter(filterPart);
                });
            } else {
                this._filters = [this._processFilter(filter)];
            }
        },

        // TODO -- We should do a single setTimeout here instead, just in case N clients request a refresh
        // in response to callbacks.
        refresh: function (options) {
            this._raiseRefreshStartEvent();

            var self = this,
                success = function (entitySet, entities, totalCount) {
                    self._bindToEntitySet(entitySet);
                    // TODO -- This means that we can't do CUD on this data source until we've refreshed it once.
                    // Allow client to pass the entityType, which would allow us to _not_ require a refresh to prime.

                    self._completeRefresh(entities, totalCount, options);
                };

            this._dataContext.__load({
                // TODO -- Combine these into an object at construction time.
                queryName: this._queryName,
                queryParameters: this._queryParameters,

                filters: this._filters,
                sort: this._sort,
                skip: this._skip,
                take: this._take,
                includeTotalCount: this._includeTotalCount
            }, success);
            // TODO -- Need failure callback here.  How does it affect events/callbacks?
        },

        commitChanges: function () {
            this._dataContext.commitChanges();
        },

        // Private methods

        _onCommitStart: function () {
            this._raiseCommitStartEvent();
        },

        _onCommit: function () {
            this._raiseCommitEvent();
        },

        _raiseCommitStartEvent: function () {
            this._raiseEvent("commitStart");
        },

        _raiseCommitEvent: function () {
            // There is almost zero value to this callback.  Clients can, as easily, react to the "commit succeeded" callback.
            // Further, notifying the client of pushed notifications requiring a refresh should be handled in a first class way
            // and shouldn't be confused with "results stale" (which we're reserving for the LDS to notify that the local query
            // would return a different result).
            // Often, clients will want to know the difference between a commit of internal versus external changes anyways.
            //// this._raiseResultsStaleEvent();

            this._raiseEvent("commit");
        },

        _bindToEntitySet: function (entitySet) {
            if (entitySet !== this._entitySet) {
                if (this._entitySet) {
                    this._entitySet.removeObserver(this._entitySetObserver);
                }
                this._entitySet = entitySet;
                if (this._entitySet) {
                    this._entitySet.addObserver(this._entitySetObserver);
                }
            }
        },

        _getEntityType: function () {
            return this._entitySet.__getEntityType();
        }
    };

    MSD.RemoteDataSource = MSD.deriveClass(MSD.DataSource, ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// <reference path="RemoteDataSource.js" />
(function (global, MSD, undefined) {

    var ctor = function (dataContext, entityType, clientEntities, handleAddEntity, flushDeferredEventsFromAdd) {
        var options = {
            dataContext: dataContext,
            entityCollection: clientEntities
        };
        this._super.constructor.call(this, null, null, entityType, options);
        var entitySet = dataContext.getEntitySet(entityType);
        this._bindToEntitySet(entitySet);

        this._handleAddEntity = handleAddEntity;
        this._flushDeferredEventsFromAdd = flushDeferredEventsFromAdd;

        // This can move to DataSource if/when it supports starting with non-empty clientEntities.
        this.__syncToNewEntities();
    };

    // TODO -- I chose to derive from RemoteDataSource merely out of convenience.  It keeps a dedicated "_entities" cache
    // that is separate from "_clientEntities", which isn't needed for AssociatedEntitiesDataSource.
    var instanceMembers = {

        // Public methods

        getTotalEntityCount: function () {
            return this._entities.length;
        },

        setSort: function (options) {
            throw "Associated entities collections don't support query.";
        },

        setFilter: function (filter) {
            throw "Associated entities collections don't support query.";
        },

        setPaging: function (options) {
            throw "Associated entities collections don't support query.";
        },

        refresh: function (options) {
            throw "Associated entities collections refresh implicitly.";
        },


        // Internal methods

        __syncToNewEntities: function () {
            // TODO -- We sync _entities differently than the other data source types.
            this._entities = $.map(this._clientEntities, function (entity) {
                return { entity: entity };
            });
        },


        // Private methods

        _addEntity: function (entity) {
            // TODO -- We don't care about our base class' tracking of entities added
            // directly to this data source.  We sync _entities via a different mechanism.
            //// this._entities.push({ entity: entity, added: true });

            // Delegate to the appropriate entity set to make the addition and synchronize all affected 
            // associated child entity collections.
            this._handleAddEntity(entity);
        },

        _purgeEntity: function (entityToPurge) {
            // TODO -- We sync _entities via a different mechanism.
        },

        _flushDeferredEvents: function () {
            // Used to flush deferred events from _handleAddEntity and order them _after_ all "arrayChange"
            // events on our _clientEntities.
            this._flushDeferredEventsFromAdd();
        }

        // TODO -- Make array removals from "_clientEntities" null out foreign key values.
    };

    MSD.AssociatedEntitiesDataSource = MSD.deriveClass(MSD.RemoteDataSource, ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// <reference path="DataSource.js" />
/// <dictionary target='comment'>recompute</dictionary>
(function (global, MSD, undefined) {

    var ctor = function (inputData, options) {

        // Should be in DataSource, but AssociatedEntitiesDataSource supplies non-empty entityCollection.
        if (options && options.entityCollection && options.entityCollection.length !== 0) {
            throw "NYI -- Currently, entity collection must be empty to bind to a data source.";
        }
        
        // TODO: We currently admit either a DataSource or an EntitySet as input here.
        // We should either capture this with (preferably) some EntityCollection base class or have 
        // explicit, separate treatment below for DataSource, EntitySet and (missing) raw array.
        var isDataSource = "_lastRefreshTotalEntityCount" in inputData; // TODO: Should be inputData instanceof MSD.DataSource, but our defineClass doesn't seem to allow for this.
        if (!(isDataSource || inputData instanceof MSD.EntitySet)) {
            throw "Currently, local data sources only accept DataSource- and EntitySet-typed input data.";
        }
        this._inputDataSourceOrEntitySet = inputData;

        // Optional query options
        this._sort = null;
        this._filter = null;

        // State
        this._refreshAllInProgress = false;
        this._resultsStale = true;
        this._deferredResultsStaleEvent = false;
        
        var self = this;
        this._observer = {
            refreshStart: function () { self._onRefreshStart(); },
            refresh: function (entities, totalCount) { self._onRefresh(entities); },
            propertyChanged: function (entity, property, newValue) { self._onPropertyChanged(entity, property, newValue); },
            entityStateChanged: function (entity, state) { self._onEntityStateChanged(entity, state); },
            commit: function () { self._onCommit(); },
            resultsStale: function () { self._onResultsStale(); }
        };
        inputData.addObserver(this._observer);
        
        this._innerArrayChangeHandler = function (changeEvent, change) {
            self._handleInnerCollectionChange(changeEvent, change);
        };
        $([inputData.getEntities()]).bind("arrayChange", this._innerArrayChangeHandler);
        // TODO -- This had used "first: true" when we had our hacked-up jQuery events.
        // We need to revisit consistency in LDS and RDS and see that these DS's don't sync
        // their state due to events that clients see too.

        var inputDataForBase = {
            getEntities: function () { return inputData.getEntities(); },
            getEntityState: function (entity) { return inputData.getEntityState(entity); },
            getEntityValidationRules: function () { return inputData.getEntityValidationRules(); },  // TODO: Will be null for EntitySet.
            isPropertyChanged: function (entity, propertyName) { return inputData.isPropertyChanged(entity, propertyName); },
            getErrors: function () { return inputData.getErrors(); },
            revertChange: function (entity, propertyName) { return inputData.revertChange(entity, propertyName); },
            revertChanges: function (all) { return inputData.revertChanges(all); },  // TODO: EntitySet doesn't mind "all".
            getEntityId: function (entity) { return inputData.getEntityId(entity); }
        };
        this._super.constructor.call(this, inputDataForBase, options);
    };

    var instanceMembers = {

        // Public methods

        dispose: function () {
            MSD.DataSource.prototype.dispose.apply(this);
            if (this._observer) {
                this._inputDataSourceOrEntitySet.removeObserver(this._observer);
                this._observer = null;
            }
            if (this._innerArrayChangeHandler) {
                $([this._inputDataSourceOrEntitySet.getEntities()]).unbind("arrayChange", this._innerArrayChangeHandler);
                this._innerArrayChangeHandler = null;
            }
        },

        getDataContext: function () {
            // TODO: EntitySet doesn't have "getDataContext".
            return this._inputDataSourceOrEntitySet.getDataContext && this._inputDataSourceOrEntitySet.getDataContext();
        },

        setSort: function (sort) {
            // TODO -- Should really raise "results stale" event when changed (throughout).
            // TODO -- Validate sort specification?
            this._sort = sort;
        },

        setFilter: function (filter) {
            // TODO -- Should really raise "results stale" event when changed (throughout).
            this._filter = filter && this._createFilterFunction(filter);
        },

        refresh: function (options) {
            this._raiseRefreshStartEvent();

            var self = this;

            if (options && !!options.all && this._inputDataSourceOrEntitySet.refresh) {  // TODO: EntitySet doesn't have "refresh".
                // N.B.  "all" is a helper, in the sense that it saves a client from doing a serverDataSource.refresh and then,
                // in response to serverDataSource.onRefresh, calling localDataSource.refresh.  Also, it allows the app to listen
                // on refreshStart/refresh events from this LDS alone (and not the inner SDS as well).
                this._refreshAllInProgress = true;
                this._inputDataSourceOrEntitySet.refresh({
                    all: true,
                    completed: function (entities) {
                        completeRefresh(entities);
                        self._refreshAllInProgress = false;
                    }
                });
            } else {
                // We do this refresh asynchronously so that, if this refresh was called during a callback,
                // the app receives remaining callbacks first, before the new batch of callbacks with respect to this refresh.
                // TODO -- We should only refresh once in response to N>1 "refresh" calls.
                setTimeout(function () { completeRefresh(self._inputDataSourceOrEntitySet.getEntities()); });
            }

            function completeRefresh(entities) {
                self._resultsStale = false;

                var results = self._applyQuery(entities);
                self._completeRefresh(results.entities, results.totalCount, options);
            };
        },

        areResultsStale: function () {
            return this._resultsStale;
        },

        // Private methods

        _onRefreshStart: function () {
            // Don't translate this directly to the client.  Only client-inflicted refreshes should event to the client.
            //// this._raiseRefreshStartEvent();
        },

        _onRefresh: function (entities) {
            if (this._refreshAllInProgress) {
                // We don't want to event "results stale" due to a "refresh all".
                // Rather, we want to issue "refresh completed".
                return;
            }

            if (!this._resultsStale) {
                // TODO -- We could consider a "inner data source refreshed" callback
                // instead, to save the app the cost of diff'ing here.  Many apps will
                // just explicitly refresh in response to "results stale" anyways.

                var resultsStale = false,
                results = this._applyQuery(entities);

                if (this.totalCount !== results.totalCount) {
                    resultsStale = true;
                } else {
                    var oldEntities = this.getEntities(),
                    newEntities = results.entities;

                    if (oldEntities.length !== newEntities.length) {
                        resultsStale = true;
                    } else {
                        for (var i = 0; i < oldEntities.length; i++) {
                            // Reference comparison is enough here.  "property changed" catches deeper causes of "results stale".
                            if (oldEntities[i] !== newEntities[i]) {
                                resultsStale = true;
                                break;
                            }
                        }
                    }
                }

                if (resultsStale) {
                    // Don't recompute and translate into "on refresh" event.  That would violate the principle
                    // of having only direct refresh or add/delete calls change the result set membership.
                    this._setResultsStale();
                }
            }
        },

        _normalizePropertyValue: function (entity, property) {
            // TODO -- Should do this based on metadata and return default value of the correct scalar type.
            return entity[property] || "";
        },

        _onPropertyChanged: function (entity, property, newValue) {
            MSD.DataSource.prototype._onPropertyChanged.apply(this, arguments);

            if (this._refreshAllInProgress) {
                // We don't want to event "results stale" due to a "refresh all".
                // Rather, we want to issue "refresh completed".
                return;
            }

            if (!this._resultsStale) {
                var resultsStale = false;
                if (this._filter && !this._filter(entity)) {
                    resultsStale = true;
                }
                if (this._getCachedEntityByEntity(entity) && this._sort) {
                    if ($.isFunction(this._sort)) {
                        resultsStale = true;
                    } else if (Object.prototype.toString.call(this._sort) === "[object Array]") {
                        resultsStale = $.grep(this._sort, function (sortPart) {
                            return sortPart.property === property;
                        }).length > 0;
                    } else {
                        resultsStale = this._sort.property === property;
                    }
                }

                if (resultsStale) {
                    // No need to defer events here for internal/client updates.
                    // This will already have been done by the EntitySet or LocalDataSource
                    // raising "property changed".
                    this._setResultsStale();

                    // TODO -- Clients see the same events that we react to here, so we have
                    // a consistency problems.  We need to take steps to ensure that we sync "results stale"
                    // before clients receive events.
                }
            }
        },

        _onCommit: function () {
            // "Commit succeeded" should probably only be sourced from server data source.
            // The only benefit would be for an LDS chain to be treated just as a lone SDS.
            //// this._raiseCommitEvent();
        },

        _onResultsStale: function () {
            // Clients need to listen on their inner data source directly for indications that
            // it needs to be refreshed.
            // Since server data sources won't know whether a given update invalidates their cached
            // result, any "results stale" signal from server data source will be too pessimistic.
            // Further, a commit of internal versus external changes could both plausibly trigger some
            // "results stale" from the inner data source, and it's more likely that the app wants
            // to know the difference here.
            //// this._raiseResultsStaleEvent();
        },

        _raiseResultsStaleEvent: function () {
            this._deferredResultsStaleEvent = false;
            this._raiseEvent("resultsStale");
        },

        _createFilterFunction: function (filter) {
            var self = this;

            if (Object.prototype.toString.call(filter) === "[object Array]") {
                var comparisonFunctions = $.map(filter, function (filterPart) {
                    return createFunction(filterPart);
                });
                return function (entity) {
                    for (var i = 0; i < comparisonFunctions.length; i++) {
                        if (!comparisonFunctions[i](entity)) {
                            return false;
                        }
                    }
                    return true;
                };
            } else {
                return createFunction(filter);
            }

            function createFunction(filterPart) {
                if ($.isFunction(filterPart)) {
                    return filterPart;
                }

                var processedFilter = self._processFilter(filterPart),
                filterProperty = processedFilter.filterProperty,
                filterOperator = processedFilter.filterOperator,
                filterValue = processedFilter.filterValue;

                var comparer;
                switch (filterOperator) {
                    case "<": comparer = function (propertyValue) { return propertyValue < filterValue; }; break;
                    case "<=": comparer = function (propertyValue) { return propertyValue <= filterValue; }; break;
                    case "==": comparer = function (propertyValue) { return propertyValue == filterValue; }; break;
                    case "!=": comparer = function (propertyValue) { return propertyValue != filterValue; }; break;
                    case ">=": comparer = function (propertyValue) { return propertyValue >= filterValue; }; break;
                    case ">": comparer = function (propertyValue) { return propertyValue > filterValue; }; break;
                    case "Contains":
                        comparer = function (propertyValue) {
                            if (typeof propertyValue === "string" && typeof filterValue === "string") {
                                propertyValue = propertyValue.toLowerCase();
                                filterValue = filterValue.toLowerCase();
                        }
                        return propertyValue.indexOf(filterValue) >= 0;
                    };
                        break;
                    default: throw "Unrecognized filter operator.";
                };

                return function (entity) {
                    // Can't trust added entities, for instance, to have all required property values.
                    var propertyValue = self._normalizePropertyValue(entity, filterProperty);
                    return comparer(propertyValue);
                };
            };
        },

        _getSortFunction: function () {
            var self = this;
            if (!this._sort) {
                return null;
            } else if ($.isFunction(this._sort)) {
                return this._sort;
            } else if (Object.prototype.toString.call(this._sort) === "[object Array]") {
                var sortFunction;
                $.each(this._sort, function (unused, sortPart) {
                    var sortPartFunction = getSortPartFunction(sortPart);
                    if (!sortFunction) {
                        sortFunction = sortPartFunction;
                    } else {
                        sortFunction = function (sortPartFunction1, sortPartFunction2) {
                            return function (entity1, entity2) {
                                var result = sortPartFunction1(entity1, entity2);
                                return result === 0 ? sortPartFunction2(entity1, entity2) : result;
                            };
                        } (sortFunction, sortPartFunction);
                    }
                });
                return sortFunction;
            } else {
                return getSortPartFunction(this._sort);
            }

            function getSortPartFunction(sortPart) {
                return function (entity1, entity2) {
                    var isAscending = (sortPart.direction || "asc").toLowerCase().indexOf("asc") === 0,
                    propertyValue1 = self._normalizePropertyValue(entity1, sortPart.property),
                    propertyValue2 = self._normalizePropertyValue(entity2, sortPart.property);
                    if (propertyValue1 == propertyValue2) {
                        return 0;
                    } else if (propertyValue1 > propertyValue2) {
                        return isAscending ? 1 : -1;
                    } else {
                        return isAscending ? -1 : 1;
                    }
                };
            }
        },

        _applyQuery: function (entities) {
            var self = this;

            var filteredEntities;
            if (this._filter) {
                filteredEntities = $.grep(entities, function (entity, index) {
                    return self._filter(entity);
                });
            } else {
                filteredEntities = entities;
            }

            var sortFunction = this._getSortFunction(),
            sortedEntities;
            if (sortFunction) {
                sortedEntities = filteredEntities.sort(sortFunction);
            } else {
                sortedEntities = filteredEntities;
            }

            var skip = this._skip || 0,
            pagedEntities = sortedEntities.slice(skip);
            if (this._take) {
                pagedEntities = pagedEntities.slice(0, this._take);
            }
            var totalCount = this._includeTotalCount ? sortedEntities.length : undefined;

            return { entities: pagedEntities, totalCount: totalCount };
        },

        _handleInnerCollectionChange: function (changeEvent, eventArguments) {
            if (this._refreshAllInProgress) {
                // We don't want to event "results stale" due to a "refresh all".
                // Rather, we want to issue "refresh completed".
                return;
            }

            if (!this._resultsStale) {
                // See if the inner array change should cause us to raise the "stale query result" event.
                var self = this,
                resultsStale = false,
                anyEntitiesInCache = function (entities) {
                    return $.grep(entities, function (entity) {
                        return !!self._getCachedEntityByEntity(entity);
                    }).length > 0;
                };

                switch (eventArguments.change) {
                    case "insert":
                        var addedEntities = eventArguments.items;
                        if (addedEntities.length > 0) {
                            var filter = this._filter || function (entity) { return true; },
                            anyExternallyAddedEntitiesMatchFilter = $.grep(addedEntities, function (entity) {
                                return filter(entity) &&
                                    $.grep(self._entities, function (cachedEntity) {
                                        return cachedEntity.entity === entity && cachedEntity.added;
                                    }).length === 0;
                            }).length > 0;
                            if (anyExternallyAddedEntitiesMatchFilter) {
                                resultsStale = true;
                            }
                        }
                        break;

                    case "remove":
                        if (anyEntitiesInCache(eventArguments.items)) {
                            resultsStale = true;
                        }
                        break;

                    case "refresh":
                        // TODO: These events carry oldItems/newItems.  We could compute resultsStale more
                        // precisely based on these.
                        resultsStale = true;
                        break;

                    case "move":
                        if (!this._sort && anyEntitiesInCache(eventArguments.items)) {
                            resultsStale = true;
                        }
                        break;

                    default:
                        throw "Unknown array operation '" + eventArguments.change + "'.";
                }

                if (resultsStale) {
                    // No need to defer the "results stale" event here, as the intent of this function
                    // is to weed out self-inflicted collection changes that this data source merely
                    // forwarded to the input entity collection.
                    this._setResultsStale();

                    // TODO -- Clients see the same events that we react to here, so we have
                    // a consistency problems.  We need to take steps to ensure that we sync "results stale"
                    // before clients receive events.
                }
            }
        },

        _setResultsStale: function (deferEvent) {
            /// <param name="deferEvent" optional="true"></param>

            this._resultsStale = true;

            if (deferEvent) {
                // TODO -- We should be using this codepath for the arrayChange and propertyChange cases.
                // These cases should be changed so that they sync "results stale" _before_ events are
                // delivered to clients.
                this._deferredResultsStaleEvent = true;
            } else {
                this._raiseResultsStaleEvent();
            }
        },

        _addEntity: function (entity) {
            MSD.DataSource.prototype._addEntity.apply(this, arguments);

            if (this._filter && !this._filter(entity)) {
                this._setResultsStale();
            }
        },

        _flushDeferredEvents: function () {
            if (this._deferredResultsStaleEvent) {
                this._raiseResultsStaleEvent();
            }
        }
    };

    MSD.LocalDataSource = MSD.deriveClass(MSD.DataSource, ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
/// File: jquery.datasource.js
/// <dictionary target='comment'>serializable</dictionary>
(function ($) {

    $.fn.extend({
        dataSource: function (options) {
            /// <summary>
            /// Creates a data source for the selected element using the specified options. When no options
            /// are provided, it will return a data source instance for the selected element if one already
            /// exists.
            /// </summary>
            /// <param name="options" type="Object">
            /// The options used to create a new data source
            /// &#10;
            /// &#10; The following common value options are available
            /// &#10; 'filter': Filters the selected array. ex. { property: "myName", operator: "==", value: myValue }
            /// &#10; 'sort': Sorts the selected array. ex. { property: "myName", direction: "ascending" }
            /// &#10; 'paging': Pages the selected array. ex. { skip: 10, take: 5, includeTotalCount: true }
            /// &#10; 
            /// &#10; The following remote value options are available
            /// &#10; 'serviceUrl': The root service url
            /// &#10; 'queryName': The query operation name
            /// &#10; 'queryParameters': The parameters to pass to the query operation
            /// &#10; 'format': The service data format. Valid options include: "OData"
            /// &#10; 'bufferChanges': A value that indicates whether changes are buffered or committed immediately
            /// &#10; 'entityType': The type of entity the operation returns
            /// &#10; 'dataContext': The DataContext to use with the data source
            /// &#10; 
            /// &#10; The following event options are available
            /// &#10; 'refreshing': This event is raised at the start of a refresh operation
            /// &#10; 'refresh': This event is raised upon successful completion of a refresh operation
            /// &#10; 'entityStateChange': This event is raised when an entity's state changes
            /// &#10; 'queryResultsStale' (local): This event is raised when the local query is no longer valid
            /// &#10; 'committing' (remote): This event is raised at the start of a commit operation
            /// &#10; 'commit' (remote): This event is raised upon successful completion of a commit operation
            /// </param>
            /// <returns type="Object">A data source for the selected element</returns>
            // TODO -- I pick off the first element here, following a pattern in jquery.validate.js.  Confirm that this is ok.
            return makeDataSource(this[0], options);
        }
    });

    $.dataSource = function (options) {
        /// <summary>
        /// Creates a data source using the specified options
        /// </summary>
        /// <param name="options" type="Object">
        /// The options used to create a new data source
        /// &#10;
        /// &#10; The following common value options are available
        /// &#10; 'filter': Filters the selected array. ex. { property: "myName", operator: "==", value: myValue }
        /// &#10; 'sort': Sorts the selected array. ex. { property: "myName", direction: "ascending" }
        /// &#10; 'paging': Pages the selected array. ex. { skip: 10, take: 5, includeTotalCount: true }
        /// &#10; 
        /// &#10; The following remote value options are available
        /// &#10; 'serviceUrl': The root service url
        /// &#10; 'queryName': The query operation name
        /// &#10; 'queryParameters': The parameters to pass to the query operation
        /// &#10; 'format': The service data format. Valid options include: "OData"
        /// &#10; 'bufferChanges': A value that indicates whether changes are buffered or committed immediately
        /// &#10; 'entityType': The type of entity the operation returns
        /// &#10; 'dataContext': The DataContext to use with the data source
        /// &#10; 
        /// &#10; The following event options are available
        /// &#10; 'refreshing': This event is raised at the start of a refresh operation
        /// &#10; 'refresh': This event is raised upon successful completion of a refresh operation
        /// &#10; 'entityStateChange': This event is raised when an entity's state changes
        /// &#10; 'queryResultsStale' (local): This event is raised when the local query is no longer valid
        /// &#10; 'committing' (remote): This event is raised at the start of a commit operation
        /// &#10; 'commit' (remote): This event is raised upon successful completion of a commit operation
        /// </param>
        /// <returns type="Object">A data source for the specified options</returns>
        if (!options) {
            throw "Provide construction options to $.dataSource";
        } else {
            var entityCollection = options.entityCollection || [];
            delete options.entityCollection;
            return makeDataSource(entityCollection, options);
        }
    };

    function makeDataSource(entityCollection, options) {
        if (options) {
            var currentDataSource = entityCollection.__dataSource__;
            if (currentDataSource) {
                currentDataSource.destroy();
            }

            entityCollection.__dataSource__ = createDataSource(entityCollection, options);
        }

        return entityCollection.__dataSource__;
    }

    // TODO -- For associated entities collections, we need to be able to ascribe a data source
    // to a collection from within our data source code itself.  Need to refactor to fix this
    // break in layering.
    $.dataSource.wrapHack = function (entityCollection, dataSource) {
        entityCollection.__dataSource__ = wrap(dataSource);
    };
    $.dataSource.unwrapHack = function (entityCollection) {
        return entityCollection.__dataSource__._dataSource;
    };

    function createDataSource(entityCollection, options) {
        var MSD = Microsoft.ServiceModel.DomainServices,
            dataSource;
        if (options.serviceUrl) {
            dataSource = new MSD.RemoteDataSource(options.serviceUrl, options.queryName, options.entityType, {
                format: options.format,
                queryParameters: options.queryParameters,
                bufferChanges: options.bufferChanges,
                dataContext: options.dataContext,
                entityCollection: entityCollection
            });
        } else if (options.inputData) {
            var inputData = options.inputData.__dataSource__ ? $.dataSource.unwrapHack(options.inputData) : options.inputData;
            dataSource = new MSD.LocalDataSource(inputData, {
                entityCollection: entityCollection
            });
        } else {
            throw "Must provide either server parameters or an input array.";
        }

        return wrap(dataSource).options(options);
    }

    function wrap(dataSource) {
        var refreshingHandler, refreshHandler, committingHandler, commitHandler, queryResultsStaleHandler, entityStateChangeHandler;
        var result;
        var observer = {
                refreshStart: function () {
                    $(result).trigger("datasourcerefreshing", arguments);
                    if (refreshingHandler) {
                        refreshingHandler.apply(null, arguments);
                    }
                },
                refresh: function () {
                    $(result).trigger("datasourcerefresh", arguments);
                    if (refreshHandler) {
                        refreshHandler.apply(null, arguments);
                    }
                },
                commitStart: function () {
                    $(result).trigger("datasourcecommitting", arguments);
                    if (committingHandler) {
                        committingHandler.apply(null, arguments);
                    }
                },
                commit: function () {
                    $(result).trigger("datasourcecommit", arguments);
                    if (commitHandler) {
                        commitHandler.apply(null, arguments);
                    }
                },
                resultsStale: function () {
                    $(result).trigger("datasourcequeryresultsstale", arguments);
                    if (queryResultsStaleHandler) {
                        queryResultsStaleHandler.apply(null, arguments);
                    }
                },
                entityStateChanged: function () {
                    $(result).trigger("datasourceentitiestatechange", arguments);
                    if (entityStateChangeHandler) {
                        entityStateChangeHandler.apply(null, arguments);
                    }
                }
            };
        dataSource.addObserver(observer);

        // TODO -- Make this a function and add the methods, so the result isn't serializable.
        result = {
            applyLocalQuery: function (options) {
                var MSD = Microsoft.ServiceModel.DomainServices;
                var newDataSource = new MSD.LocalDataSource(dataSource);

                var entityCollection = newDataSource.getEntities();
                entityCollection.__dataSource__ = wrap(newDataSource);

                entityCollection.__dataSource__.options(options);

                return entityCollection;
                // TODO -- Consider returning the data source here, so this can be fluent.
                // We'd need a getEntities() as well, as typically the app wants to chain data source
                // operators and then assign the eventual array to a var.
            },

            options: function (options) {
                /// <summary>Applies one or more options to the data source</summary>
                /// <param name="options" type="Object">
                /// The options to apply
                /// &#10;
                /// &#10; The following common value options are available
                /// &#10; 'filter': Filters the selected array. ex. { property: "myName", operator: "==", value: myValue }
                /// &#10; 'sort': Sorts the selected array. ex. { property: "myName", direction: "ascending" }
                /// &#10; 'paging': Pages the selected array. ex. { skip: 10, take: 5, includeTotalCount: true }
                /// &#10; 
                /// &#10; The following remote value options are available
                /// &#10; 'serviceUrl': The root service url
                /// &#10; 'queryName': The query operation name
                /// &#10; 'queryParameters': The parameters to pass to the query operation
                /// &#10; 'format': The service data format. Valid options include: "OData"
                /// &#10; 'bufferChanges': A value that indicates whether changes are buffered or committed immediately
                /// &#10; 'entityType': The type of entity the operation returns
                /// &#10; 'dataContext': The DataContext to use with the data source
                /// &#10; 
                /// &#10; The following event options are available
                /// &#10; 'refreshing': This event is raised at the start of a refresh operation
                /// &#10; 'refresh': This event is raised upon successful completion of a refresh operation
                /// &#10; 'entityStateChange': This event is raised when an entity's state changes
                /// &#10; 'queryResultsStale' (local): This event is raised when the local query is no longer valid
                /// &#10; 'committing' (remote): This event is raised at the start of a commit operation
                /// &#10; 'commit' (remote): This event is raised upon successful completion of a commit operation
                /// </param>
                applyOptions(options);
                return this;
            },

            option: function (option, value) {
                /// <summary>Applies an option to the data source</summary>
                /// <param name="option" type="string">
                /// The name of the option to apply
                /// &#10;
                /// &#10; The following common value options are available
                /// &#10; 'filter': Filters the selected array. ex. { property: "myName", operator: "==", value: myValue }
                /// &#10; 'sort': Sorts the selected array. ex. { property: "myName", direction: "ascending" }
                /// &#10; 'paging': Pages the selected array. ex. { skip: 10, take: 5, includeTotalCount: true }
                /// &#10; 
                /// &#10; The following remote value options are available
                /// &#10; 'serviceUrl': The root service url
                /// &#10; 'queryName': The query operation name
                /// &#10; 'queryParameters': The parameters to pass to the query operation
                /// &#10; 'format': The service data format. Valid options include: "OData"
                /// &#10; 'bufferChanges': A value that indicates whether changes are buffered or committed immediately
                /// &#10; 'entityType': The type of entity the operation returns
                /// &#10; 'dataContext': The DataContext to use with the data source
                /// &#10; 
                /// &#10; The following event options are available
                /// &#10; 'refreshing': This event is raised at the start of a refresh operation
                /// &#10; 'refresh': This event is raised upon successful completion of a refresh operation
                /// &#10; 'entityStateChange': This event is raised when an entity's state changes
                /// &#10; 'queryResultsStale' (local): This event is raised when the local query is no longer valid
                /// &#10; 'committing' (remote): This event is raised at the start of a commit operation
                /// &#10; 'commit' (remote): This event is raised upon successful completion of a commit operation
                /// </param>
                /// <param name="value" type="Object">The value of the option</param>
                /// <returns type="Object">The data source</returns>
                applyOption(option, value);
                return this;
            },

            refresh: function (options) {
                /// <summary>Refreshes the data source</summary>
                /// <param name="options" type="Object">
                /// The options for refreshing the data source. This parameter is optional.
                /// &#10; 'completed': This event is raised upon successful completion of a refresh operation
                /// </param>
                /// <returns type="Object">The data source</returns>
                if (options) {
                    var hasDataSourceOptions;
                    $.each(options, function (key, value) {
                        if (key !== "all" && key !== "completed") {
                            hasDataSourceOptions = true;
                        }
                    });
                    if (hasDataSourceOptions) {
                        // "applyOptions" trounces all the old query options, so only call if we really have options here.
                        applyOptions(options);
                    }
                }
                dataSource.refresh(options);
                return this;
            },

            commitChanges: function () {
                dataSource.commitChanges();
                return this;
            },

            revertChanges: function (entity, propertyName) {
                if (typeof entity === "object") {
                    dataSource.revertChange(entity, propertyName);
                } else {
                    // Assume "entity" is boolean-typed "all".
                    dataSource.revertChanges(!!entity);
                }
                return this;
            },

            destroy: function () {
                if (dataSource) {
                    delete dataSource.getEntities().__dataSource__;

                    dataSource.removeObserver(observer);
                    dataSource.dispose();

                    dataSource = null;
                }

                return this;
            },

            getEntities: function () {
                return dataSource.getEntities();
            },

            getEntityState: function (entity) {
                return dataSource.getEntityState(entity);
            },

            getEntityValidationRules: function () {
                return dataSource.getEntityValidationRules();
            },

            getErrors: function () {
                return dataSource.getErrors();
            },

            isPropertyChanged: function (entity, propertyName) {
                return dataSource.isPropertyChanged(entity, propertyName);
            },

            dataContext: function () {
                return dataSource.getDataContext();
            },

            getTotalCount: function () {
                return dataSource.getTotalEntityCount();
            },

            getEntityId: function (entity) {
                return dataSource.getEntityId(entity);
            },

            // TODO -- Merely supports unwrapHack above.  Remove.
            _dataSource: dataSource
        };

        return result;

        // N.B.  Null/undefined option values will unset the given option.
        function applyOptions(options) {
            options = options || {};

            $.each(["filter", "sort", "paging", "refreshing", "refresh", "committing", "commit", "queryResultsStale", "entityStateChange"], function (index, eventName) {
                applyOption(eventName, options[eventName]);
            });
        }

        function applyOption(option, value) {
            switch (option) {
                case "filter":
                    dataSource.setFilter(value);
                    break;

                case "sort":
                    dataSource.setSort(value);
                    break;

                case "paging":
                    dataSource.setPaging(value);
                    break;

                case "refreshing":
                    refreshingHandler = value;
                    break;

                case "refresh":
                    refreshHandler = value;
                    break;

                case "committing":
                    committingHandler = value;
                    break;

                case "commit":
                    commitHandler = value;
                    break;

                case "queryResultsStale":
                    queryResultsStaleHandler = value;
                    break;

                case "entityStateChange":
                    entityStateChangeHandler = value;
                    break;

                default:
                    throw "Unrecognized option '" + option + "'";
            }
        }
    }
})(jQuery);
/// <reference path="datajs-1.0.1.js" />

(function (global, MSD, undefined) {

    function transformQuery(query) {
        var queryParameters = {};

        // $filter/filter/filters -> $filter
        if (query.$filter || query.filter) {
            queryParameters.$filter = query.$filter || query.filter;
        } else if (query.filters) {
            var applyOperator = function (property, operator, value) {
                if (typeof value === "string") {
                    value = "'" + value + "'";
                }

                switch (operator) {
                    case "<": return property + " lt " + value;
                    case "<=": return property + " le " + value;
                    case "==": return property + " eq " + value;
                    case "!=": return property + " ne " + value;
                    case ">=": return property + " ge " + value;
                    case ">": return property + " gt " + value;
                    case "StartsWith": return "startswith(" + property + "," + value + ") eq true";
                    case "EndsWith": return "endswith(" + property + "," + value + ") eq true";
                    case "Contains": return "substringof(" + value + "," + property + ") eq true";
                    default: throw "The operator '" + operator + "' is not supported.";
                }
            };

            queryParameters.$filter = $.map(query.filters, function (filter, index) {
                return applyOperator(filter.filterProperty, filter.filterOperator, filter.filterValue);
            }).join(" and ");
        }

        // $orderby/orderby/sort -> $orderby
        if (query.$orderby || query.orderby) {
            queryParameters.$orderby = query.$orderby || query.orderby;
        } else if (query.sort) {
            var orderbyParameter = "",
                formatSort = function (sort) {
                    if (sort.direction === "descending") {
                        return sort.property + " desc";
                    }
                    return sort.property;
                };

            if (typeof query.sort === "string") {
                orderbyParameter = query.sort;
            } else if ($.isArray(query.sort)) {
                orderbyParameter = $.map(query.sort, function (sort, index) {
                    return formatSort(sort);
                }).join();
            } else {
                orderbyParameter = formatSort(query.sort);
            }

            queryParameters.$orderby = orderbyParameter;
        }

        // $skip/skip -> $skip
        if (query.$skip || query.skip) {
            queryParameters.$skip = query.$skip || query.skip;
        }

        // $take/take/$top/top -> $top
        if (query.$take || query.take || query.$top || query.top) {
            queryParameters.$top = query.$take || query.take || query.$top || query.top;
        }

        // $expand/expand -> $expand
        if (query.$expand || query.expand) {
            queryParameters.$expand = query.$expand || query.expand;
        }

        // $format/format -> $format
        // Value can be atom/json/xml
        if (query.$format || query.format) {
            queryParameters.$format = query.$format || query.format;
        }

        // $select/select -> $select
        if (query.$select || query.select) {
            queryParameters.$select = query.$select || query.select;
        }

        // $includeTotalCount/includeTotalCount/$inlinecount/inlinecount -> $inlinecount
        if (query.$inlinecount || query.inlinecount) {
            queryParameters.$inlinecount = query.$inlinecount || query.inlinecount;
        } else if (query.$includeTotalCount || query.includeTotalCount) {
            queryParameters.$inlinecount = "allpages";
        }

        return queryParameters;
    }

    function fromJSON(data) {
        for (var field in data) {
            var value = data[field];
            if (value === null) {
                continue;
            } else if ($.isArray(value)) {
                for (var item in value) {
                    fromJSON(item);
                }
            } else if (typeof value === "object") {
                if (isLink(value)) {
                    // Collapse link collection results
                    data[field] = value = value.results;
                }
                fromJSON(value);
            } else if (value && value.match && value.match(/\/Date\([+\-]?\d+[+\-]?\d*\)\//)) {
                data[field] = new Date(+value.replace(/\/Date\(([+\-]?\d+)[+\-]?\d*\)\//, '$1'));
            }
        }

        return data;
    }

    function isLink(data) {
        var count = 0;
        for (var field in data) {
            if (++count > 1) {
                return false;
            }
        }
        return !!data.results;
    }

    var ctor = function (serviceUrl) {
        this._serviceUrl = serviceUrl;
    }

    var instanceMembers = {

        // Public methods

        get: function (operation, parameters, queryParameters) {
            /// <summary>
            /// Asynchronously gets data from the server using the specified operation, parameters, and query
            /// </summary>
            /// <param name="operation" type="String">The operation name</param>
            /// <param name="parameters" type="Object">An object where each property is a parameter to pass to the operation. This parameter is optional.</param>
            /// <param name="queryParameters" type="Object">An object where each property is a query to pass to the operation. This parameter is optional.</param>
            /// <returns type="Promise">A Promise representing the result of the load operation</returns>

            var serviceUrl = this._serviceUrl,
                normalizedUrl = serviceUrl.substring(serviceUrl.length - 1) !== "/" ? serviceUrl + "/" : serviceUrl;

            // $.map applied to objects is supported in jQuery >= 1.6. Our current baseline is jQuery 1.5
            var parameterStrings = [];
            $.each($.extend({}, parameters, transformQuery(queryParameters || {})), function (key, value) {
                parameterStrings.push(key.toString() + "=" + value.toString());
            });
            var queryString = parameterStrings.length ? ("?" + parameterStrings.join("&")) : "";

            // Invoke the query
            var deferred = $.Deferred();
            OData.read(normalizedUrl + operation + queryString,
                function (result) {
                    deferred.resolve(result);
                },
                function (error) {
                    deferred.reject(error);
                }
            );

            // TODO: For now, the result we'll eventually return is in the native protocol form and thus should be 
            // treated as opaque to all but the "load" function below.
            return deferred;
        },

        load: function (getResult, dataContext) {
            /// <summary>
            /// Loads data from a previous "get" call into the supplied DataContext
            /// </summary>
            /// <param name="getResult" type="Object">The result from some previous invocation of "get" on this DataProvider</param>
            /// <param name="dataContext" type="DataContext">The DataContext into which the result should be loaded</param>
            /// <returns type="Array">An array containing entities merged into the DataContext</returns>
            var entities = fromJSON(getResult.results),
                resultType = entities.length && entities[0].__metadata.type;

            if (resultType) {
                dataContext.addMetadata(resultType, {
                    getServerId: function (entity) {
                        return entity.__metadata.uri;
                    }
                });
            }

            var mergedEntities = dataContext.merge(entities, resultType),
                count = getResult.__count,
                totalCount = count === undefined ? null : +count;

            return {
                entities: mergedEntities,
                type: resultType,
                totalCount: totalCount
            };
            // TODO - this may actually be the error reporting case as well...
        },

        post: function () {
            throw "Saving edits through the OData data provider is not supported.";
        }
    };

    MSD.ODataDataProvider = MSD.defineClass(ctor, instanceMembers);

})(this, Microsoft.ServiceModel.DomainServices);
