text: var util = require('../util');
  var Model = require('./Model');
  
  Model.prototype._mutate = function(segments, fn, cb) {
    cb = this.wrapCallback(cb);
    var collectionName = segments[0];
    var id = segments[1];
    if (!collectionName || !id) {
      var message = fn.name + ' must be performed under a collection ' +
        'and document id. Invalid path: ' + segments.join('.');
      return cb(new Error(message));
    }
    var doc = this.getOrCreateDoc(collectionName, id);
    var docSegments = segments.slice(2);
    return fn(doc, docSegments, cb);
  };
  
  Model.prototype.set = function() {
    var subpath, value, cb;
    if (arguments.length === 1) {
      value = arguments[0];
    } else if (arguments.length === 2) {
      subpath = arguments[0];
      value = arguments[1];
    } else {
      subpath = arguments[0];
      value = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._set(segments, value, cb);
  };
  Model.prototype._set = function(segments, value, cb) {
    segments = this._dereference(segments);
    var model = this;
    function set(doc, docSegments, fnCb) {
      var previous = doc.set(docSegments, value, fnCb);
      // On setting the entire doc, remote docs sometimes do a copy to add the
      // id without it being stored in the database by ShareJS
      if (docSegments.length === 0) value = doc.get(docSegments);
      model.emit('change', segments, [value, previous, model._pass]);
      return previous;
    }
    return this._mutate(segments, set, cb);
  };
  
  Model.prototype.setEach = function() {
    var subpath, object, cb;
    if (arguments.length === 1) {
      object = arguments[0];
    } else if (arguments.length === 2) {
      subpath = arguments[0];
      object = arguments[1];
    } else {
      subpath = arguments[0];
      object = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._setEach(segments, object, cb);
  };
  Model.prototype._setEach = function(segments, object, cb) {
    segments = this._dereference(segments);
    var group = util.asyncGroup(this.wrapCallback(cb));
    for (var key in object) {
      var value = object[key];
      this._set(segments.concat(key), value, group());
    }
  };
  
  Model.prototype.add = function() {
    var subpath, value, cb;
    if (arguments.length === 1) {
      value = arguments[0];
    } else if (arguments.length === 2) {
      if (typeof arguments[1] === 'function') {
        value = arguments[0];
        cb = arguments[1];
      } else {
        subpath = arguments[0];
        value = arguments[1];
      }
    } else {
      subpath = arguments[0];
      value = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._add(segments, value, cb);
  };
  Model.prototype._add = function(segments, value, cb) {
    if (typeof value !== 'object') {
      var message = 'add requires an object value. Invalid value: ' + value;
      cb = this.wrapCallback(cb);
      return cb(new Error(message));
    }
    var id = value.id || this.id();
    value.id = id;
    this._set(segments.concat(id), value, cb);
    return id;
  };
  
  Model.prototype.setNull = function() {
    var subpath, value, cb;
    if (arguments.length === 1) {
      value = arguments[0];
    } else if (arguments.length === 2) {
      subpath = arguments[0];
      value = arguments[1];
    } else {
      subpath = arguments[0];
      value = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._setNull(segments, value, cb);
  };
  Model.prototype._setNull = function(segments, value, cb) {
    segments = this._dereference(segments);
    var model = this;
    function setNull(doc, docSegments, fnCb) {
      var previous = doc.get(docSegments);
      if (previous != null) {
        fnCb();
        return previous;
      }
      doc.set(docSegments, value, fnCb);
      model.emit('change', segments, [value, previous, model._pass]);
      return value;
    }
    return this._mutate(segments, setNull, cb);
  };
  
  Model.prototype.del = function() {
    var subpath, cb;
    if (arguments.length === 1) {
      if (typeof arguments[0] === 'function') {
        cb = arguments[0];
      } else {
        subpath = arguments[0];
      }
    } else {
      subpath = arguments[0];
      cb = arguments[1];
    }
    var segments = this._splitPath(subpath);
    return this._del(segments, cb);
  };
  Model.prototype._del = function(segments, cb) {
    segments = this._dereference(segments);
    var model = this;
    function del(doc, docSegments, fnCb) {
      var previous = doc.del(docSegments, fnCb);
      // When deleting an entire document, also remove the reference to the
      // document object from its collection
      if (segments.length === 2) {
        var collectionName = segments[0];
        var id = segments[1];
        model.root.collections[collectionName].remove(id);
      }
      model.emit('change', segments, [void 0, previous, model._pass]);
      return previous;
    }
    return this._mutate(segments, del, cb);
  };
  
  Model.prototype.increment = function() {
    var subpath, byNumber, cb;
    if (arguments.length === 1) {
      if (typeof arguments[0] === 'function') {
        cb = arguments[0];
      } else if (typeof arguments[0] === 'number') {
        byNumber = arguments[0];
      } else {
        subpath = arguments[0];
      }
    } else if (arguments.length === 2) {
      if (typeof arguments[1] === 'function') {
        cb = arguments[1];
        if (typeof arguments[0] === 'number') {
          byNumber = arguments[0];
        } else {
          subpath = arguments[0];
        }
      } else {
        subpath = arguments[0];
        byNumber = arguments[1];
      }
    } else {
      subpath = arguments[0];
      byNumber = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._increment(segments, byNumber, cb);
  };
  Model.prototype._increment = function(segments, byNumber, cb) {
    segments = this._dereference(segments);
    if (byNumber == null) byNumber = 1;
    var model = this;
    function increment(doc, docSegments, fnCb) {
      var value = doc.increment(docSegments, byNumber, fnCb);
      var previous = value - byNumber;
      model.emit('change', segments, [value, previous, model._pass]);
      return value;
    }
    return this._mutate(segments, increment, cb);
  };
  
  Model.prototype.push = function() {
    var subpath, value, cb;
    if (arguments.length === 1) {
      value = arguments[0];
    } else if (arguments.length === 2) {
      subpath = arguments[0];
      value = arguments[1];
    } else {
      subpath = arguments[0];
      value = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._push(segments, value, cb);
  };
  Model.prototype._push = function(segments, value, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    var model = this;
    function push(doc, docSegments, fnCb) {
      var length = doc.push(docSegments, value, fnCb);
      model.emit('insert', segments, [length - 1, [value], model._pass]);
      return length;
    }
    return this._mutate(segments, push, cb);
  };
  
  Model.prototype.unshift = function() {
    var subpath, value, cb;
    if (arguments.length === 1) {
      value = arguments[0];
    } else if (arguments.length === 2) {
      subpath = arguments[0];
      value = arguments[1];
    } else {
      subpath = arguments[0];
      value = arguments[1];
      cb = arguments[2];
    }
    var segments = this._splitPath(subpath);
    return this._unshift(segments, value, cb);
  };
  Model.prototype._unshift = function(segments, value, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    var model = this;
    function unshift(doc, docSegments, fnCb) {
      var length = doc.unshift(docSegments, value, fnCb);
      model.emit('insert', segments, [0, [value], model._pass]);
      return length;
    }
    return this._mutate(segments, unshift, cb);
  };
  
  Model.prototype.insert = function() {
    var subpath, index, values, cb;
    if (arguments.length === 1) {
      throw new Error('Not enough arguments for insert');
    } else if (arguments.length === 2) {
      index = arguments[0];
      values = arguments[1];
    } else if (arguments.length === 3) {
      subpath = arguments[0];
      index = arguments[1];
      values = arguments[2];
    } else {
      subpath = arguments[0];
      index = arguments[1];
      values = arguments[2];
      cb = arguments[3];
    }
    var segments = this._splitPath(subpath);
    return this._insert(segments, +index, values, cb);
  };
  Model.prototype._insert = function(segments, index, values, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    var model = this;
    function insert(doc, docSegments, fnCb) {
      var inserted = (Array.isArray(values)) ? values : [values];
      var length = doc.insert(docSegments, index, inserted, fnCb);
      model.emit('insert', segments, [index, inserted, model._pass]);
      return length;
    }
    return this._mutate(segments, insert, cb);
  };
  
  Model.prototype.pop = function() {
    var subpath, cb;
    if (arguments.length === 1) {
      if (typeof arguments[0] === 'function') {
        cb = arguments[0];
      } else {
        subpath = arguments[0];
      }
    } else {
      subpath = arguments[0];
      cb = arguments[1];
    }
    var segments = this._splitPath(subpath);
    return this._pop(segments, cb);
  };
  Model.prototype._pop = function(segments, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    var model = this;
    function pop(doc, docSegments, fnCb) {
      var arr = doc.get(docSegments);
      var length = arr && arr.length;
      if (!length) {
        fnCb();
        return;
      }
      var value = doc.pop(docSegments, fnCb);
      model.emit('remove', segments, [length - 1, [value], model._pass]);
      return value;
    }
    return this._mutate(segments, pop, cb);
  };
  
  Model.prototype.shift = function() {
    var subpath, cb;
    if (arguments.length === 1) {
      if (typeof arguments[0] === 'function') {
        cb = arguments[0];
      } else {
        subpath = arguments[0];
      }
    } else {
      subpath = arguments[0];
      cb = arguments[1];
    }
    var segments = this._splitPath(subpath);
    return this._shift(segments, cb);
  };
  Model.prototype._shift = function(segments, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    var model = this;
    function shift(doc, docSegments, fnCb) {
      var arr = doc.get(docSegments);
      var length = arr && arr.length;
      if (!length) {
        fnCb();
        return;
      }
      var value = doc.shift(docSegments, fnCb);
      model.emit('remove', segments, [0, [value], model._pass]);
      return value;
    }
    return this._mutate(segments, shift, cb);
  };
  
  Model.prototype.remove = function() {
    var subpath, index, howMany, cb;
    if (arguments.length === 1) {
      index = arguments[0];
    } else if (arguments.length === 2) {
      if (typeof arguments[1] === 'function') {
        cb = arguments[1];
        if (typeof arguments[0] === 'number') {
          index = arguments[0];
        } else {
          subpath = arguments[0];
        }
      } else {
        if (typeof arguments[0] === 'number') {
          index = arguments[0];
          howMany = arguments[1];
        } else {
          subpath = arguments[0];
          index = arguments[1];
        }
      }
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'function') {
        cb = arguments[2];
        if (typeof arguments[0] === 'number') {
          index = arguments[0];
          howMany = arguments[1];
        } else {
          subpath = arguments[0];
          index = arguments[1];
        }
      } else {
        subpath = arguments[0];
        index = arguments[1];
        howMany = arguments[2];
      }
    } else {
      subpath = arguments[0];
      index = arguments[1];
      howMany = arguments[2];
      cb = arguments[3];
    }
    var segments = this._splitPath(subpath);
    if (index == null) index = segments.pop();
    return this._remove(segments, +index, howMany, cb);
  };
  Model.prototype._remove = function(segments, index, howMany, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    if (howMany == null) howMany = 1;
    var model = this;
    function remove(doc, docSegments, fnCb) {
      var removed = doc.remove(docSegments, index, howMany, fnCb);
      model.emit('remove', segments, [index, removed, model._pass]);
      return removed;
    }
    return this._mutate(segments, remove, cb);
  };
  
  Model.prototype.move = function() {
    var subpath, from, to, howMany, cb;
    if (arguments.length === 1) {
      throw new Error('Not enough arguments for move');
    } else if (arguments.length === 2) {
      from = arguments[0];
      to = arguments[1];
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'function') {
        from = arguments[0];
        to = arguments[1];
        cb = arguments[2];
      } else if (typeof arguments[0] === 'number') {
        from = arguments[0];
        to = arguments[1];
        howMany = arguments[2];
      } else {
        subpath = arguments[0];
        from = arguments[1];
        to = arguments[2];
      }
    } else if (arguments.length === 4) {
      if (typeof arguments[3] === 'function') {
        cb = arguments[3];
        if (typeof arguments[0] === 'number') {
          from = arguments[0];
          to = arguments[1];
          howMany = arguments[2];
        } else {
          subpath = arguments[0];
          from = arguments[1];
          to = arguments[2];
        }
      } else {
        subpath = arguments[0];
        from = arguments[1];
        to = arguments[2];
        howMany = arguments[3];
      }
    } else {
      subpath = arguments[0];
      from = arguments[1];
      to = arguments[2];
      howMany = arguments[3];
      cb = arguments[4];
    }
    var segments = this._splitPath(subpath);
    return this._move(segments, from, to, howMany, cb);
  };
  Model.prototype._move = function(segments, from, to, howMany, cb) {
    var forArrayMutator = true;
    segments = this._dereference(segments, forArrayMutator);
    if (howMany == null) howMany = 1;
    var model = this;
    function move(doc, docSegments, fnCb) {
      // Cast to numbers
      from = +from;
      to = +to;
      // Convert negative indices into positive
      if (from < 0 || to < 0) {
        var len = doc.get(docSegments).length;
        if (from < 0) from += len;
        if (to < 0) to += len;
      }
      var moved = doc.move(docSegments, from, to, howMany, fnCb);
      model.emit('move', segments, [from, to, moved.length, model._pass]);
      return moved;
    }
    return this._mutate(segments, move, cb);
  };
  
  Model.prototype.stringInsert = function() {
    var subpath, index, text, cb;
    if (arguments.length === 1) {
      throw new Error('Not enough arguments for stringInsert');
    } else if (arguments.length === 2) {
      index = arguments[0];
      text = arguments[1];
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'function') {
        index = arguments[0];
        text = arguments[1];
        cb = arguments[2];
      } else {
        subpath = arguments[0];
        index = arguments[1];
        text = arguments[2];
      }
    } else {
      subpath = arguments[0];
      index = arguments[1];
      text = arguments[2];
      cb = arguments[3];
    }
    var segments = this._splitPath(subpath);
    return this._stringInsert(segments, index, text, cb);
  };
  Model.prototype._stringInsert = function(segments, index, text, cb) {
    segments = this._dereference(segments);
    var model = this;
    function stringInsert(doc, docSegments, fnCb) {
      var previous = doc.stringInsert(docSegments, index, text, fnCb);
      var value = doc.get(docSegments);
      var pass = model.pass({$stringInsert: {index: index, text: text}})._pass;
      model.emit('change', segments, [value, previous, pass]);
      return;
    }
    return this._mutate(segments, stringInsert, cb);
  };
  
  Model.prototype.stringRemove = function() {
    var subpath, index, howMany, cb;
    if (arguments.length === 1) {
      throw new Error('Not enough arguments for stringRemove');
    } else if (arguments.length === 2) {
      index = arguments[0];
      howMany = arguments[1];
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'function') {
        index = arguments[0];
        howMany = arguments[1];
        cb = arguments[2];
      } else {
        subpath = arguments[0];
        index = arguments[1];
        howMany = arguments[2];
      }
    } else {
      subpath = arguments[0];
      index = arguments[1];
      howMany = arguments[2];
      cb = arguments[3];
    }
    var segments = this._splitPath(subpath);
    return this._stringRemove(segments, index, howMany, cb);
  };
  Model.prototype._stringRemove = function(segments, index, howMany, cb) {
    segments = this._dereference(segments);
    var model = this;
    function stringRemove(doc, docSegments, fnCb) {
      var previous = doc.stringRemove(docSegments, index, howMany, fnCb);
      var value = doc.get(docSegments);
      var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass;
      model.emit('change', segments, [value, previous, pass]);
      return;
    }
    return this._mutate(segments, stringRemove, cb);
  };
  