(function (root, factory) {
  // AMD.
  if (typeof define === 'function' && define.amd) {
    define(['exports', 'backbone', 'underscore'], factory);

  // CommonJS.
  } else if (typeof exports !== 'undefined') {
    factory(exports, require('backbone'), require('underscore'));

  // Finally, as a browser global.
  } else {
    factory(root, root.Backbone, root._);
  }
}(this, function (exports, Backbone, _) { 'use strict';
  var urlError = function () {
    throw new Error('A "url" property or function must be specified');
  };

  // Backup original sync.
  var originalSync = Backbone.sync;

  /**
   * Overrided version of backbone sync.
   */

  Backbone.sync = function (method, model, options) {
    // Default sync options.
    Backbone.syncOptions = Backbone.syncOptions || function () {
      return {};
    };

    // Default options.
    _.defaults(options || (options = {}), Backbone.syncOptions, {
        socket: Backbone.socket || window.socket
      });

    // Subscribe only on "read" method and if there is a socket.
    if (method === 'read' && options.socket) {
      subscribe(model, options);
    }

    // Fall back to original sync.
    return originalSync.apply(this, arguments);
  };

  /**
   * Subscribe for a specific url.
   *
   * @param {Backbone.Model|Backbone.Collection} model
   * @param {object} options
   */

  function subscribe(model, options) {
    _.defaults(options, {
        subscribe: model.subscribe,
        subscriptionType: model.subscriptionType,
        subscriptionKey: model.subscriptionKey
      });

    // Subscribe option must be defined.
    if (options.subscribe === undefined) return;

    // Subscription key must be defined for 'session' subscription.
    if (options.subscriptionType === 'session' &&
        options.subscriptionKey === undefined) return;

    // Ensure that we have a URL.
    var url = options.url || _.result(model, 'url') || urlError();

    // Add params.
    if (options.data) {
      url = url + '?' + Backbone.$.param(options.data);
    }

    options.headers = options.headers || {};
    options.headers['X-Subscription-Active'] = options.subscribe;
    options.headers['X-Subscription-Type'] = options.subscriptionType;
    options.headers['X-Subscription-Key'] = options.subscriptionKey;

    // Enable sync.
    enableSync(model, options.socket);

    // Update sync url.
    model.syncUrl = url;
  }

  /**
   * Enable sync on the model or collection.
   *
   * @param {Backbone.Model|Backbone.Collection} model
   */

  function enableSync(model, socket) {
    if (model.syncSocket) return;

    // Define syncSocket.
    model.syncSocket = socket;

    // Listen sync event.
    model.syncSocket.on('sync', function (msg) {
      // Normalize URL by adding trailing slash.
      var msgUrl = msg.url.replace(/\/?(\?|#|$)/, '/$1');
      var syncUrl = model.syncUrl.replace(/\/?(\?|#|$)/, '/$1');

      // Ignore if the message is not for our model.
      if (msgUrl !== syncUrl) return;

      // Trigger events on the model.
      model.trigger('sync:request', msg.data);
    });
  }
}));
