# op.js

    var _ = require('underscore');

    module.exports = function(AV) {
      /**
       * @private
       * @class
       * A AV.Op is an atomic operation that can be applied to a field in a
       * AV.Object. For example, calling <code>object.set("foo", "bar")</code>
       * is an example of a AV.Op.Set. Calling <code>object.unset("foo")</code>
       * is a AV.Op.Unset. These operations are stored in a AV.Object and
       * sent to the server as part of <code>object.save()</code> operations.
       * Instances of AV.Op should be immutable.
       *
       * You should not create subclasses of AV.Op or instantiate AV.Op
       * directly.
       */
      AV.Op = function() {
        this._initialize.apply(this, arguments);
      };

      _.extend(
        AV.Op.prototype,
        /** @lends AV.Op.prototype */ {
          _initialize: function() {},
        }
      );

      _.extend(AV.Op, {
        /**
         * To create a new Op, call AV.Op._extend();
         * @private
         */
        _extend: AV._extend,

        // A map of __op string to decoder function.
        _opDecoderMap: {},

        /**
         * Registers a function to convert a json object with an __op field into an
         * instance of a subclass of AV.Op.
         * @private
         */
        _registerDecoder: function(opName, decoder) {
          AV.Op._opDecoderMap[opName] = decoder;
        },

        /**
         * Converts a json object into an instance of a subclass of AV.Op.
         * @private
         */
        _decode: function(json) {
          var decoder = AV.Op._opDecoderMap[json.__op];
          if (decoder) {
            return decoder(json);
          } else {
            return undefined;
          }
        },
      });

      /*
       * Add a handler for Batch ops.
       */
      AV.Op._registerDecoder('Batch', function(json) {
        var op = null;
        AV._arrayEach(json.ops, function(nextOp) {
          nextOp = AV.Op._decode(nextOp);
          op = nextOp._mergeWithPrevious(op);
        });
        return op;
      });

      /**
       * @private
       * @class
       * A Set operation indicates that either the field was changed using
       * AV.Object.set, or it is a mutable container that was detected as being
       * changed.
       */
      AV.Op.Set = AV.Op._extend(
        /** @lends AV.Op.Set.prototype */ {
          _initialize: function(value) {
            this._value = value;
          },

          /**
           * Returns the new value of this field after the set.
           */
          value: function() {
            return this._value;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return AV._encode(this.value());
          },

          _mergeWithPrevious: function(previous) {
            return this;
          },

          _estimate: function(oldValue) {
            return this.value();
          },
        }
      );

      /**
       * A sentinel value that is returned by AV.Op.Unset._estimate to
       * indicate the field should be deleted. Basically, if you find _UNSET as a
       * value in your object, you should remove that key.
       */
      AV.Op._UNSET = {};

      /**
       * @private
       * @class
       * An Unset operation indicates that this field has been deleted from the
       * object.
       */
      AV.Op.Unset = AV.Op._extend(
        /** @lends AV.Op.Unset.prototype */ {
          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return { __op: 'Delete' };
          },

          _mergeWithPrevious: function(previous) {
            return this;
          },

          _estimate: function(oldValue) {
            return AV.Op._UNSET;
          },
        }
      );

      AV.Op._registerDecoder('Delete', function(json) {
        return new AV.Op.Unset();
      });

      /**
       * @private
       * @class
       * An Increment is an atomic operation where the numeric value for the field
       * will be increased by a given amount.
       */
      AV.Op.Increment = AV.Op._extend(
        /** @lends AV.Op.Increment.prototype */ {
          _initialize: function(amount) {
            this._amount = amount;
          },

          /**
           * Returns the amount to increment by.
           * @return {Number} the amount to increment by.
           */
          amount: function() {
            return this._amount;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return { __op: 'Increment', amount: this._amount };
          },

          _mergeWithPrevious: function(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(this.amount());
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(previous.value() + this.amount());
            } else if (previous instanceof AV.Op.Increment) {
              return new AV.Op.Increment(this.amount() + previous.amount());
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate: function(oldValue) {
            if (!oldValue) {
              return this.amount();
            }
            return oldValue + this.amount();
          },
        }
      );

      AV.Op._registerDecoder('Increment', function(json) {
        return new AV.Op.Increment(json.amount);
      });

      /**
       * @private
       * @class
       * BitAnd is an atomic operation where the given value will be bit and to the
       * value than is stored in this field.
       */
      AV.Op.BitAnd = AV.Op._extend(
        /** @lends AV.Op.BitAnd.prototype */ {
          _initialize(value) {
            this._value = value;
          },

          value() {
            return this._value;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON() {
            return { __op: 'BitAnd', value: this.value() };
          },

          _mergeWithPrevious(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(0);
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(previous.value() & this.value());
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate(oldValue) {
            return oldValue & this.value();
          },
        }
      );

      AV.Op._registerDecoder('BitAnd', function(json) {
        return new AV.Op.BitAnd(json.value);
      });

      /**
       * @private
       * @class
       * BitOr is an atomic operation where the given value will be bit and to the
       * value than is stored in this field.
       */
      AV.Op.BitOr = AV.Op._extend(
        /** @lends AV.Op.BitOr.prototype */ {
          _initialize(value) {
            this._value = value;
          },

          value() {
            return this._value;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON() {
            return { __op: 'BitOr', value: this.value() };
          },

          _mergeWithPrevious(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(this.value());
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(previous.value() | this.value());
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate(oldValue) {
            return oldValue | this.value();
          },
        }
      );

      AV.Op._registerDecoder('BitOr', function(json) {
        return new AV.Op.BitOr(json.value);
      });

      /**
       * @private
       * @class
       * BitXor is an atomic operation where the given value will be bit and to the
       * value than is stored in this field.
       */
      AV.Op.BitXor = AV.Op._extend(
        /** @lends AV.Op.BitXor.prototype */ {
          _initialize(value) {
            this._value = value;
          },

          value() {
            return this._value;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON() {
            return { __op: 'BitXor', value: this.value() };
          },

          _mergeWithPrevious(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(this.value());
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(previous.value() ^ this.value());
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate(oldValue) {
            return oldValue ^ this.value();
          },
        }
      );

      AV.Op._registerDecoder('BitXor', function(json) {
        return new AV.Op.BitXor(json.value);
      });

      /**
       * @private
       * @class
       * Add is an atomic operation where the given objects will be appended to the
       * array that is stored in this field.
       */
      AV.Op.Add = AV.Op._extend(
        /** @lends AV.Op.Add.prototype */ {
          _initialize: function(objects) {
            this._objects = objects;
          },

          /**
           * Returns the objects to be added to the array.
           * @return {Array} The objects to be added to the array.
           */
          objects: function() {
            return this._objects;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return { __op: 'Add', objects: AV._encode(this.objects()) };
          },

          _mergeWithPrevious: function(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(this.objects());
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(this._estimate(previous.value()));
            } else if (previous instanceof AV.Op.Add) {
              return new AV.Op.Add(previous.objects().concat(this.objects()));
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate: function(oldValue) {
            if (!oldValue) {
              return _.clone(this.objects());
            } else {
              return oldValue.concat(this.objects());
            }
          },
        }
      );

      AV.Op._registerDecoder('Add', function(json) {
        return new AV.Op.Add(AV._decode(json.objects));
      });

      /**
       * @private
       * @class
       * AddUnique is an atomic operation where the given items will be appended to
       * the array that is stored in this field only if they were not already
       * present in the array.
       */
      AV.Op.AddUnique = AV.Op._extend(
        /** @lends AV.Op.AddUnique.prototype */ {
          _initialize: function(objects) {
            this._objects = _.uniq(objects);
          },

          /**
           * Returns the objects to be added to the array.
           * @return {Array} The objects to be added to the array.
           */
          objects: function() {
            return this._objects;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return { __op: 'AddUnique', objects: AV._encode(this.objects()) };
          },

          _mergeWithPrevious: function(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return new AV.Op.Set(this.objects());
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(this._estimate(previous.value()));
            } else if (previous instanceof AV.Op.AddUnique) {
              return new AV.Op.AddUnique(this._estimate(previous.objects()));
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate: function(oldValue) {
            if (!oldValue) {
              return _.clone(this.objects());
            } else {
              // We can't just take the _.uniq(_.union(...)) of oldValue and
              // this.objects, because the uniqueness may not apply to oldValue
              // (especially if the oldValue was set via .set())
              var newValue = _.clone(oldValue);
              AV._arrayEach(this.objects(), function(obj) {
                if (obj instanceof AV.Object && obj.id) {
                  var matchingObj = _.find(newValue, function(anObj) {
                    return anObj instanceof AV.Object && anObj.id === obj.id;
                  });
                  if (!matchingObj) {
                    newValue.push(obj);
                  } else {
                    var index = _.indexOf(newValue, matchingObj);
                    newValue[index] = obj;
                  }
                } else if (!_.contains(newValue, obj)) {
                  newValue.push(obj);
                }
              });
              return newValue;
            }
          },
        }
      );

      AV.Op._registerDecoder('AddUnique', function(json) {
        return new AV.Op.AddUnique(AV._decode(json.objects));
      });

      /**
       * @private
       * @class
       * Remove is an atomic operation where the given objects will be removed from
       * the array that is stored in this field.
       */
      AV.Op.Remove = AV.Op._extend(
        /** @lends AV.Op.Remove.prototype */ {
          _initialize: function(objects) {
            this._objects = _.uniq(objects);
          },

          /**
           * Returns the objects to be removed from the array.
           * @return {Array} The objects to be removed from the array.
           */
          objects: function() {
            return this._objects;
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            return { __op: 'Remove', objects: AV._encode(this.objects()) };
          },

          _mergeWithPrevious: function(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              return previous;
            } else if (previous instanceof AV.Op.Set) {
              return new AV.Op.Set(this._estimate(previous.value()));
            } else if (previous instanceof AV.Op.Remove) {
              return new AV.Op.Remove(_.union(previous.objects(), this.objects()));
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate: function(oldValue) {
            if (!oldValue) {
              return [];
            } else {
              var newValue = _.difference(oldValue, this.objects());
              // If there are saved AV Objects being removed, also remove them.
              AV._arrayEach(this.objects(), function(obj) {
                if (obj instanceof AV.Object && obj.id) {
                  newValue = _.reject(newValue, function(other) {
                    return other instanceof AV.Object && other.id === obj.id;
                  });
                }
              });
              return newValue;
            }
          },
        }
      );

      AV.Op._registerDecoder('Remove', function(json) {
        return new AV.Op.Remove(AV._decode(json.objects));
      });

      /**
       * @private
       * @class
       * A Relation operation indicates that the field is an instance of
       * AV.Relation, and objects are being added to, or removed from, that
       * relation.
       */
      AV.Op.Relation = AV.Op._extend(
        /** @lends AV.Op.Relation.prototype */ {
          _initialize: function(adds, removes) {
            this._targetClassName = null;

            var self = this;

            var pointerToId = function(object) {
              if (object instanceof AV.Object) {
                if (!object.id) {
                  throw new Error(
                    "You can't add an unsaved AV.Object to a relation."
                  );
                }
                if (!self._targetClassName) {
                  self._targetClassName = object.className;
                }
                if (self._targetClassName !== object.className) {
                  throw new Error(
                    'Tried to create a AV.Relation with 2 different types: ' +
                      self._targetClassName +
                      ' and ' +
                      object.className +
                      '.'
                  );
                }
                return object.id;
              }
              return object;
            };

            this.relationsToAdd = _.uniq(_.map(adds, pointerToId));
            this.relationsToRemove = _.uniq(_.map(removes, pointerToId));
          },

          /**
           * Returns an array of unfetched AV.Object that are being added to the
           * relation.
           * @return {Array}
           */
          added: function() {
            var self = this;
            return _.map(this.relationsToAdd, function(objectId) {
              var object = AV.Object._create(self._targetClassName);
              object.id = objectId;
              return object;
            });
          },

          /**
           * Returns an array of unfetched AV.Object that are being removed from
           * the relation.
           * @return {Array}
           */
          removed: function() {
            var self = this;
            return _.map(this.relationsToRemove, function(objectId) {
              var object = AV.Object._create(self._targetClassName);
              object.id = objectId;
              return object;
            });
          },

          /**
           * Returns a JSON version of the operation suitable for sending to AV.
           * @return {Object}
           */
          toJSON: function() {
            var adds = null;
            var removes = null;
            var self = this;
            var idToPointer = function(id) {
              return {
                __type: 'Pointer',
                className: self._targetClassName,
                objectId: id,
              };
            };
            var pointers = null;
            if (this.relationsToAdd.length > 0) {
              pointers = _.map(this.relationsToAdd, idToPointer);
              adds = { __op: 'AddRelation', objects: pointers };
            }

            if (this.relationsToRemove.length > 0) {
              pointers = _.map(this.relationsToRemove, idToPointer);
              removes = { __op: 'RemoveRelation', objects: pointers };
            }

            if (adds && removes) {
              return { __op: 'Batch', ops: [adds, removes] };
            }

            return adds || removes || {};
          },

          _mergeWithPrevious: function(previous) {
            if (!previous) {
              return this;
            } else if (previous instanceof AV.Op.Unset) {
              throw new Error("You can't modify a relation after deleting it.");
            } else if (previous instanceof AV.Op.Relation) {
              if (
                previous._targetClassName &&
                previous._targetClassName !== this._targetClassName
              ) {
                throw new Error(
                  'Related object must be of class ' +
                    previous._targetClassName +
                    ', but ' +
                    this._targetClassName +
                    ' was passed in.'
                );
              }
              var newAdd = _.union(
                _.difference(previous.relationsToAdd, this.relationsToRemove),
                this.relationsToAdd
              );
              var newRemove = _.union(
                _.difference(previous.relationsToRemove, this.relationsToAdd),
                this.relationsToRemove
              );

              var newRelation = new AV.Op.Relation(newAdd, newRemove);
              newRelation._targetClassName = this._targetClassName;
              return newRelation;
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },

          _estimate: function(oldValue, object, key) {
            if (!oldValue) {
              var relation = new AV.Relation(object, key);
              relation.targetClassName = this._targetClassName;
            } else if (oldValue instanceof AV.Relation) {
              if (this._targetClassName) {
                if (oldValue.targetClassName) {
                  if (oldValue.targetClassName !== this._targetClassName) {
                    throw new Error(
                      'Related object must be a ' +
                        oldValue.targetClassName +
                        ', but a ' +
                        this._targetClassName +
                        ' was passed in.'
                    );
                  }
                } else {
                  oldValue.targetClassName = this._targetClassName;
                }
              }
              return oldValue;
            } else {
              throw new Error('Op is invalid after previous op.');
            }
          },
        }
      );

      AV.Op._registerDecoder('AddRelation', function(json) {
        return new AV.Op.Relation(AV._decode(json.objects), []);
      });
      AV.Op._registerDecoder('RemoveRelation', function(json) {
        return new AV.Op.Relation([], AV._decode(json.objects));
      });
    };