//! backbone-delta.js 0.3.1, https://github.com/Two-Screen/backbone-delta
//! © 2012 Stéphan Kochen, Angry Bytes. MIT licensed.

(function() {

// Look for Underscore.js.
var _;
if (typeof(require) !== 'undefined') {
    _ = require('underscore');
}
else {
    _ = window._;
}

// Extend a Backbone.js instance. The option `noPatch` will prevent
// monkey-patching, and only add methods.
var extend = function(Backbone, options) {
    options || (options = {});

    var Model = Backbone.Model;
    var ModelProto = Model.prototype;
    var Collection = Backbone.Collection;
    var CollectionProto = Collection.prototype;

    // Reset the model's attributes.
    //
    // This is similar to set, but will also unset attributes not in `attrs`.
    ModelProto.reset = function(attrs, options) {
        var now = this.attributes, unset = {}, attr;
        options || (options = {});
        if (!attrs) return this;
        if (attrs instanceof Model) attrs = attrs.attributes;
        attrs = _.clone(attrs);

        // Collect attributes to unset.
        for (attr in now) {
            if (!_.has(attrs, attr)) {
                attrs[attr] = void 0;
                unset[attr] = true;
            }
        }

        // Apply changes.
        if (!this.set(attrs, options)) return false;
        for (attr in unset) {
            delete now[attr];
        }

        return this;
    };

    // Variant of `Model#fetch` that uses `Model#reset`.
    ModelProto.fetchReset = function(options) {
        options = options ? _.clone(options) : {};
        if (options.parse === void 0) options.parse = true;
        var success = options.success;
        options.success = function(model, resp, options) {
            var parsed = model.parse(resp, options);
            if (!model.reset(parsed, options)) return false;
            if (success) success(model, resp);
        };
        return this.sync('read', this, options);
    };

    // Reset the collection, applying small changes without a `reset` event.
    //
    // Models are regarded the same if their IDs match, in which case, the
    // attributes will be reset. Other models will be added or removed.
    //
    // If no matching models are found at all, this does a normal reset.
    CollectionProto.delta = function(models, options) {
        var haveId = {}, matching = [], added = [], removed, model;
        options || (options = {});
        models = _.isArray(models) ? models.slice() : [models];

        // Validate the new models, and look for matching existing models.
        for (i = 0, length = models.length; i < length; i++) {
            model = models[i] = this._prepareModel(models[i], options);
            if (!model) {
                throw new Error("Can't add an invalid model to a collection");
            }
            haveId[model.id] = true;
            (this.get(model.id) ? matching : added).push(model);
        }

        // Short-circuit to normal reset, if possible.
        if (matching.length === 0) {
            return this.reset(models, options);
        }

        // Determine which models are to be removed.
        removed = this.select(function(model) {
            return !haveId[model.id];
        });

        // Apply changes.
        this.remove(removed, options);
        _.each(matching, function(model) {
            this.get(model).reset(model, options);
        }, this);
        this.add(added, options);

        return this;
    };

    // Variant of `Collection#fetch` that uses `Collection#delta`.
    CollectionProto.fetchDelta = function(options) {
        options = options ? _.clone(options) : {};
        if (options.parse === void 0) options.parse = true;
        var success = options.success;
        options.success = function(collection, resp, options) {
            var parsed = collection.parse(resp, options);
            collection.delta(parsed, options);
            if (success) success(collection, resp, options);
        };
        return this.sync('read', this, options);
    };

    // Patch `fetch` methods to add options.
    if (!options.noPatch) {
        var origModelFetch = ModelProto.fetch;
        ModelProto.fetch = function(options) {
            if (options && options.reset) {
                return this.fetchReset(options);
            }
            else {
                return origModelFetch.call(this, options);
            }
        };

        var origCollectionFetch = CollectionProto.fetch;
        CollectionProto.fetch = function(options) {
            if (options && options.delta) {
                return this.fetchDelta(options);
            }
            else {
                return origCollectionFetch.call(this, options);
            }
        };
    }

    return Backbone;
};

// Inherit from Backbone.js and create subclasses of Model and Collection.
var inherit = function(Backbone, options) {
    var ctor = function() {};
    ctor.prototype = Backbone;
    var sub = new ctor();

    sub.Model = Backbone.Model.extend();
    sub.Collection = Backbone.Collection.extend({ model: sub.Model });

    return extend(sub, options);
};

// Export.
var BBDelta;
if (typeof(require) !== 'undefined') {
    BBDelta = inherit(require('backbone'));
    module.exports = BBDelta;
}
else {
    BBDelta = inherit(window.Backbone);
    window.BBDelta = BBDelta;
}
BBDelta.extend = extend;
BBDelta.inherit = inherit;

})();