text: const _ = require('underscore');
  const AVError = require('./error');
  const Promise = require('./promise');
  const { _request } = require('./request');
  const {
    isNullOrUndefined,
    ensureArray,
    transformFetchOptions,
    setValue,
    findValue,
    isPlainObject,
  } = require('./utils');
  
  const recursiveToPointer = value => {
    if (_.isArray(value)) return value.map(recursiveToPointer);
    if (isPlainObject(value)) return _.mapObject(value, recursiveToPointer);
    if (_.isObject(value) && value._toPointer) return value._toPointer();
    return value;
  };
  
  const RESERVED_KEYS = ['objectId', 'createdAt', 'updatedAt'];
  const checkReservedKey = key => {
    if (RESERVED_KEYS.indexOf(key) !== -1) {
      throw new Error(`key[${key}] is reserved`);
    }
  };
  
  const handleBatchResults = results => {
    const firstError = _.find(results, result => result instanceof Error);
    if (!firstError) {
      return results;
    }
    const error = new AVError(firstError.code, firstError.message);
    error.results = results;
    throw error;
  };
  
  // Helper function to get a value from a Backbone object as a property
  // or as a function.
  function getValue(object, prop) {
    if (!(object && object[prop])) {
      return null;
    }
    return _.isFunction(object[prop]) ? object[prop]() : object[prop];
  }
  
  // AV.Object is analogous to the Java AVObject.
  // It also implements the same interface as a Backbone model.
  
  module.exports = function(AV) {
    /**
     * Creates a new model with defined attributes. A client id (cid) is
     * automatically generated and assigned for you.
     *
     * <p>You won't normally call this method directly.  It is recommended that
     * you use a subclass of <code>AV.Object</code> instead, created by calling
     * <code>extend</code>.</p>
     *
     * <p>However, if you don't want to use a subclass, or aren't sure which
     * subclass is appropriate, you can use this form:<pre>
     *     var object = new AV.Object("ClassName");
     * </pre>
     * That is basically equivalent to:<pre>
     *     var MyClass = AV.Object.extend("ClassName");
     *     var object = new MyClass();
     * </pre></p>
     *
     * @param {Object} attributes The initial set of data to store in the object.
     * @param {Object} options A set of Backbone-like options for creating the
     *     object.  The only option currently supported is "collection".
     * @see AV.Object.extend
     *
     * @class
     *
     * <p>The fundamental unit of AV data, which implements the Backbone Model
     * interface.</p>
     */
    AV.Object = function(attributes, options) {
      // Allow new AV.Object("ClassName") as a shortcut to _create.
      if (_.isString(attributes)) {
        return AV.Object._create.apply(this, arguments);
      }
  
      attributes = attributes || {};
      if (options && options.parse) {
        attributes = this.parse(attributes);
        attributes = this._mergeMagicFields(attributes);
      }
      var defaults = getValue(this, 'defaults');
      if (defaults) {
        attributes = _.extend({}, defaults, attributes);
      }
      if (options && options.collection) {
        this.collection = options.collection;
      }
  
      this._serverData = {}; // The last known data for this object from cloud.
      this._opSetQueue = [{}]; // List of sets of changes to the data.
      this._flags = {};
      this.attributes = {}; // The best estimate of this's current data.
  
      this._hashedJSON = {}; // Hash of values of containers at last save.
      this._escapedAttributes = {};
      this.cid = _.uniqueId('c');
      this.changed = {};
      this._silent = {};
      this._pending = {};
      this.set(attributes, { silent: true });
      this.changed = {};
      this._silent = {};
      this._pending = {};
      this._hasData = true;
      this._previousAttributes = _.clone(this.attributes);
      this.initialize.apply(this, arguments);
    };
  
    /**
     * @lends AV.Object.prototype
     * @property {String} id The objectId of the AV Object.
     */
  
    /**
     * Saves the given list of AV.Object.
     * If any error is encountered, stops and calls the error handler.
     *
     * @example
     * AV.Object.saveAll([object1, object2, ...]).then(function(list) {
     *   // All the objects were saved.
     * }, function(error) {
     *   // An error occurred while saving one of the objects.
     * });
     *
     * @param {Array} list A list of <code>AV.Object</code>.
     */
    AV.Object.saveAll = function(list, options) {
      return AV.Object._deepSaveAsync(list, null, options);
    };
  
    /**
     * Fetch the given list of AV.Object.
     *
     * @param {AV.Object[]} objects A list of <code>AV.Object</code>
     * @param {AuthOptions} options
     * @return {Promise.<AV.Object[]>} The given list of <code>AV.Object</code>, updated
     */
  
    AV.Object.fetchAll = (objects, options) =>
      Promise.resolve()
        .then(() =>
          _request(
            'batch',
            null,
            null,
            'POST',
            {
              requests: _.map(objects, object => {
                if (!object.className)
                  throw new Error('object must have className to fetch');
                if (!object.id) throw new Error('object must have id to fetch');
                if (object.dirty())
                  throw new Error('object is modified but not saved');
                return {
                  method: 'GET',
                  path: `/1.1/classes/${object.className}/${object.id}`,
                };
              }),
            },
            options
          )
        )
        .then(function(response) {
          const results = _.map(objects, function(object, i) {
            if (response[i].success) {
              const fetchedAttrs = object.parse(response[i].success);
              object._cleanupUnsetKeys(fetchedAttrs);
              object._finishFetch(fetchedAttrs);
              return object;
            }
            if (response[i].success === null) {
              return new AVError(AVError.OBJECT_NOT_FOUND, 'Object not found.');
            }
            return new AVError(response[i].error.code, response[i].error.error);
          });
          return handleBatchResults(results);
        });
  
    // Attach all inheritable methods to the AV.Object prototype.
    _.extend(
      AV.Object.prototype,
      AV.Events,
      /** @lends AV.Object.prototype */ {
        _fetchWhenSave: false,
  
        /**
         * Initialize is an empty function by default. Override it with your own
         * initialization logic.
         */
        initialize: function() {},
  
        /**
         * Set whether to enable fetchWhenSave option when updating object.
         * When set true, SDK would fetch the latest object after saving.
         * Default is false.
         *
         * @deprecated use AV.Object#save with options.fetchWhenSave instead
         * @param {boolean} enable  true to enable fetchWhenSave option.
         */
        fetchWhenSave: function(enable) {
          console.warn(
            'AV.Object#fetchWhenSave is deprecated, use AV.Object#save with options.fetchWhenSave instead.'
          );
          if (!_.isBoolean(enable)) {
            throw new Error('Expect boolean value for fetchWhenSave');
          }
          this._fetchWhenSave = enable;
        },
  
        /**
         * Returns the object's objectId.
         * @return {String} the objectId.
         */
        getObjectId: function() {
          return this.id;
        },
  
        /**
         * Returns the object's createdAt attribute.
         * @return {Date}
         */
        getCreatedAt: function() {
          return this.createdAt;
        },
  
        /**
         * Returns the object's updatedAt attribute.
         * @return {Date}
         */
        getUpdatedAt: function() {
          return this.updatedAt;
        },
  
        /**
         * Returns a JSON version of the object.
         * @return {Object}
         */
        toJSON: function(key, holder, seenObjects = []) {
          return this._toFullJSON(seenObjects, false);
        },
  
        /**
         * Returns a JSON version of the object with meta data.
         * Inverse to {@link AV.parseJSON}
         * @since 3.0.0
         * @return {Object}
         */
        toFullJSON(seenObjects = []) {
          return this._toFullJSON(seenObjects);
        },
  
        _toFullJSON: function(seenObjects, full = true) {
          var json = _.clone(this.attributes);
          if (_.isArray(seenObjects)) {
            var newSeenObjects = seenObjects.concat(this);
          }
          AV._objectEach(json, function(val, key) {
            json[key] = AV._encode(val, newSeenObjects, undefined, full);
          });
          AV._objectEach(this._operations, function(val, key) {
            json[key] = val;
          });
  
          if (_.has(this, 'id')) {
            json.objectId = this.id;
          }
          _(['createdAt', 'updatedAt']).each(key => {
            if (_.has(this, key)) {
              const val = this[key];
              json[key] = _.isDate(val) ? val.toJSON() : val;
            }
          });
          if (full) {
            json.__type = 'Object';
            if (_.isArray(seenObjects) && seenObjects.length)
              json.__type = 'Pointer';
            json.className = this.className;
          }
          return json;
        },
  
        /**
         * Updates _hashedJSON to reflect the current state of this object.
         * Adds any changed hash values to the set of pending changes.
         * @private
         */
        _refreshCache: function() {
          var self = this;
          if (self._refreshingCache) {
            return;
          }
          self._refreshingCache = true;
          AV._objectEach(this.attributes, function(value, key) {
            if (value instanceof AV.Object) {
              value._refreshCache();
            } else if (_.isObject(value)) {
              if (self._resetCacheForKey(key)) {
                self.set(key, new AV.Op.Set(value), { silent: true });
              }
            }
          });
          delete self._refreshingCache;
        },
  
        /**
         * Returns true if this object has been modified since its last
         * save/refresh.  If an attribute is specified, it returns true only if that
         * particular attribute has been modified since the last save/refresh.
         * @param {String} attr An attribute name (optional).
         * @return {Boolean}
         */
        dirty: function(attr) {
          this._refreshCache();
  
          var currentChanges = _.last(this._opSetQueue);
  
          if (attr) {
            return currentChanges[attr] ? true : false;
          }
          if (!this.id) {
            return true;
          }
          if (_.keys(currentChanges).length > 0) {
            return true;
          }
          return false;
        },
  
        /**
         * Gets a Pointer referencing this Object.
         * @private
         */
        _toPointer: function() {
          // if (!this.id) {
          //   throw new Error("Can't serialize an unsaved AV.Object");
          // }
          return {
            __type: 'Pointer',
            className: this.className,
            objectId: this.id,
          };
        },
  
        /**
         * Gets the value of an attribute.
         * @param {String} attr The string name of an attribute.
         */
        get: function(attr) {
          switch (attr) {
            case 'objectId':
              return this.id;
            case 'createdAt':
            case 'updatedAt':
              return this[attr];
            default:
              return this.attributes[attr];
          }
        },
  
        /**
         * Gets a relation on the given class for the attribute.
         * @param {String} attr The attribute to get the relation for.
         * @return {AV.Relation}
         */
        relation: function(attr) {
          var value = this.get(attr);
          if (value) {
            if (!(value instanceof AV.Relation)) {
              throw new Error('Called relation() on non-relation field ' + attr);
            }
            value._ensureParentAndKey(this, attr);
            return value;
          } else {
            return new AV.Relation(this, attr);
          }
        },
  
        /**
         * Gets the HTML-escaped value of an attribute.
         */
        escape: function(attr) {
          var html = this._escapedAttributes[attr];
          if (html) {
            return html;
          }
          var val = this.attributes[attr];
          var escaped;
          if (isNullOrUndefined(val)) {
            escaped = '';
          } else {
            escaped = _.escape(val.toString());
          }
          this._escapedAttributes[attr] = escaped;
          return escaped;
        },
  
        /**
         * Returns <code>true</code> if the attribute contains a value that is not
         * null or undefined.
         * @param {String} attr The string name of the attribute.
         * @return {Boolean}
         */
        has: function(attr) {
          return !isNullOrUndefined(this.attributes[attr]);
        },
  
        /**
         * Pulls "special" fields like objectId, createdAt, etc. out of attrs
         * and puts them on "this" directly.  Removes them from attrs.
         * @param attrs - A dictionary with the data for this AV.Object.
         * @private
         */
        _mergeMagicFields: function(attrs) {
          // Check for changes of magic fields.
          var model = this;
          var specialFields = ['objectId', 'createdAt', 'updatedAt'];
          AV._arrayEach(specialFields, function(attr) {
            if (attrs[attr]) {
              if (attr === 'objectId') {
                model.id = attrs[attr];
              } else if (
                (attr === 'createdAt' || attr === 'updatedAt') &&
                !_.isDate(attrs[attr])
              ) {
                model[attr] = AV._parseDate(attrs[attr]);
              } else {
                model[attr] = attrs[attr];
              }
              delete attrs[attr];
            }
          });
          return attrs;
        },
  
        /**
         * Returns the json to be sent to the server.
         * @private
         */
        _startSave: function() {
          this._opSetQueue.push({});
        },
  
        /**
         * Called when a save fails because of an error. Any changes that were part
         * of the save need to be merged with changes made after the save. This
         * might throw an exception is you do conflicting operations. For example,
         * if you do:
         *   object.set("foo", "bar");
         *   object.set("invalid field name", "baz");
         *   object.save();
         *   object.increment("foo");
         * then this will throw when the save fails and the client tries to merge
         * "bar" with the +1.
         * @private
         */
        _cancelSave: function() {
          var failedChanges = _.first(this._opSetQueue);
          this._opSetQueue = _.rest(this._opSetQueue);
          var nextChanges = _.first(this._opSetQueue);
          AV._objectEach(failedChanges, function(op, key) {
            var op1 = failedChanges[key];
            var op2 = nextChanges[key];
            if (op1 && op2) {
              nextChanges[key] = op2._mergeWithPrevious(op1);
            } else if (op1) {
              nextChanges[key] = op1;
            }
          });
          this._saving = this._saving - 1;
        },
  
        /**
         * Called when a save completes successfully. This merges the changes that
         * were saved into the known server data, and overrides it with any data
         * sent directly from the server.
         * @private
         */
        _finishSave: function(serverData) {
          // Grab a copy of any object referenced by this object. These instances
          // may have already been fetched, and we don't want to lose their data.
          // Note that doing it like this means we will unify separate copies of the
          // same object, but that's a risk we have to take.
          var fetchedObjects = {};
          AV._traverse(this.attributes, function(object) {
            if (object instanceof AV.Object && object.id && object._hasData) {
              fetchedObjects[object.id] = object;
            }
          });
  
          var savedChanges = _.first(this._opSetQueue);
          this._opSetQueue = _.rest(this._opSetQueue);
          this._applyOpSet(savedChanges, this._serverData);
          this._mergeMagicFields(serverData);
          var self = this;
          AV._objectEach(serverData, function(value, key) {
            self._serverData[key] = AV._decode(value, key);
  
            // Look for any objects that might have become unfetched and fix them
            // by replacing their values with the previously observed values.
            var fetched = AV._traverse(self._serverData[key], function(object) {
              if (object instanceof AV.Object && fetchedObjects[object.id]) {
                return fetchedObjects[object.id];
              }
            });
            if (fetched) {
              self._serverData[key] = fetched;
            }
          });
          this._rebuildAllEstimatedData();
          const opSetQueue = this._opSetQueue.map(_.clone);
          this._refreshCache();
          this._opSetQueue = opSetQueue;
          this._saving = this._saving - 1;
        },
  
        /**
         * Called when a fetch or login is complete to set the known server data to
         * the given object.
         * @private
         */
        _finishFetch: function(serverData, hasData) {
          // Clear out any changes the user might have made previously.
          this._opSetQueue = [{}];
  
          // Bring in all the new server data.
          this._mergeMagicFields(serverData);
          var self = this;
          AV._objectEach(serverData, function(value, key) {
            self._serverData[key] = AV._decode(value, key);
          });
  
          // Refresh the attributes.
          this._rebuildAllEstimatedData();
  
          // Clear out the cache of mutable containers.
          this._refreshCache();
          this._opSetQueue = [{}];
  
          this._hasData = hasData;
        },
  
        /**
         * Applies the set of AV.Op in opSet to the object target.
         * @private
         */
        _applyOpSet: function(opSet, target) {
          var self = this;
          AV._objectEach(opSet, function(change, key) {
            const [value, actualTarget, actualKey] = findValue(target, key);
            setValue(target, key, change._estimate(value, self, key));
            if (actualTarget && actualTarget[actualKey] === AV.Op._UNSET) {
              delete actualTarget[actualKey];
            }
          });
        },
  
        /**
         * Replaces the cached value for key with the current value.
         * Returns true if the new value is different than the old value.
         * @private
         */
        _resetCacheForKey: function(key) {
          var value = this.attributes[key];
          if (
            _.isObject(value) &&
            !(value instanceof AV.Object) &&
            !(value instanceof AV.File)
          ) {
            var json = JSON.stringify(recursiveToPointer(value));
            if (this._hashedJSON[key] !== json) {
              var wasSet = !!this._hashedJSON[key];
              this._hashedJSON[key] = json;
              return wasSet;
            }
          }
          return false;
        },
  
        /**
         * Populates attributes[key] by starting with the last known data from the
         * server, and applying all of the local changes that have been made to that
         * key since then.
         * @private
         */
        _rebuildEstimatedDataForKey: function(key) {
          var self = this;
          delete this.attributes[key];
          if (this._serverData[key]) {
            this.attributes[key] = this._serverData[key];
          }
          AV._arrayEach(this._opSetQueue, function(opSet) {
            var op = opSet[key];
            if (op) {
              const [value, actualTarget, actualKey, firstKey] = findValue(
                self.attributes,
                key
              );
              setValue(self.attributes, key, op._estimate(value, self, key));
              if (actualTarget && actualTarget[actualKey] === AV.Op._UNSET) {
                delete actualTarget[actualKey];
              }
              self._resetCacheForKey(firstKey);
            }
          });
        },
  
        /**
         * Populates attributes by starting with the last known data from the
         * server, and applying all of the local changes that have been made since
         * then.
         * @private
         */
        _rebuildAllEstimatedData: function() {
          var self = this;
  
          var previousAttributes = _.clone(this.attributes);
  
          this.attributes = _.clone(this._serverData);
          AV._arrayEach(this._opSetQueue, function(opSet) {
            self._applyOpSet(opSet, self.attributes);
            AV._objectEach(opSet, function(op, key) {
              self._resetCacheForKey(key);
            });
          });
  
          // Trigger change events for anything that changed because of the fetch.
          AV._objectEach(previousAttributes, function(oldValue, key) {
            if (self.attributes[key] !== oldValue) {
              self.trigger('change:' + key, self, self.attributes[key], {});
            }
          });
          AV._objectEach(this.attributes, function(newValue, key) {
            if (!_.has(previousAttributes, key)) {
              self.trigger('change:' + key, self, newValue, {});
            }
          });
        },
  
        /**
         * Sets a hash of model attributes on the object, firing
         * <code>"change"</code> unless you choose to silence it.
         *
         * <p>You can call it with an object containing keys and values, or with one
         * key and value.  For example:</p>
         *
         * @example
         * gameTurn.set({
         *   player: player1,
         *   diceRoll: 2
         * });
         *
         * game.set("currentPlayer", player2);
         *
         * game.set("finished", true);
         *
         * @param {String} key The key to set.
         * @param {Any} value The value to give it.
         * @param {Object} [options]
         * @param {Boolean} [options.silent]
         * @return {AV.Object} self if succeeded, throws if the value is not valid.
         * @see AV.Object#validate
         */
        set: function(key, value, options) {
          var attrs;
          if (_.isObject(key) || isNullOrUndefined(key)) {
            attrs = _.mapObject(key, function(v, k) {
              checkReservedKey(k);
              return AV._decode(v, k);
            });
            options = value;
          } else {
            attrs = {};
            checkReservedKey(key);
            attrs[key] = AV._decode(value, key);
          }
  
          // Extract attributes and options.
          options = options || {};
          if (!attrs) {
            return this;
          }
          if (attrs instanceof AV.Object) {
            attrs = attrs.attributes;
          }
  
          // If the unset option is used, every attribute should be a Unset.
          if (options.unset) {
            AV._objectEach(attrs, function(unused_value, key) {
              attrs[key] = new AV.Op.Unset();
            });
          }
  
          // Apply all the attributes to get the estimated values.
          var dataToValidate = _.clone(attrs);
          var self = this;
          AV._objectEach(dataToValidate, function(value, key) {
            if (value instanceof AV.Op) {
              dataToValidate[key] = value._estimate(
                self.attributes[key],
                self,
                key
              );
              if (dataToValidate[key] === AV.Op._UNSET) {
                delete dataToValidate[key];
              }
            }
          });
  
          // Run validation.
          this._validate(attrs, options);
  
          options.changes = {};
          var escaped = this._escapedAttributes;
  
          // Update attributes.
          AV._arrayEach(_.keys(attrs), function(attr) {
            var val = attrs[attr];
  
            // If this is a relation object we need to set the parent correctly,
            // since the location where it was parsed does not have access to
            // this object.
            if (val instanceof AV.Relation) {
              val.parent = self;
            }
  
            if (!(val instanceof AV.Op)) {
              val = new AV.Op.Set(val);
            }
  
            // See if this change will actually have any effect.
            var isRealChange = true;
            if (
              val instanceof AV.Op.Set &&
              _.isEqual(self.attributes[attr], val.value)
            ) {
              isRealChange = false;
            }
  
            if (isRealChange) {
              delete escaped[attr];
              if (options.silent) {
                self._silent[attr] = true;
              } else {
                options.changes[attr] = true;
              }
            }
  
            var currentChanges = _.last(self._opSetQueue);
            currentChanges[attr] = val._mergeWithPrevious(currentChanges[attr]);
            self._rebuildEstimatedDataForKey(attr);
  
            if (isRealChange) {
              self.changed[attr] = self.attributes[attr];
              if (!options.silent) {
                self._pending[attr] = true;
              }
            } else {
              delete self.changed[attr];
              delete self._pending[attr];
            }
          });
  
          if (!options.silent) {
            this.change(options);
          }
          return this;
        },
  
        /**
         * Remove an attribute from the model, firing <code>"change"</code> unless
         * you choose to silence it. This is a noop if the attribute doesn't
         * exist.
         * @param key {String} The key.
         */
        unset: function(attr, options) {
          options = options || {};
          options.unset = true;
          return this.set(attr, null, options);
        },
  
        /**
         * Atomically increments the value of the given attribute the next time the
         * object is saved. If no amount is specified, 1 is used by default.
         *
         * @param key {String} The key.
         * @param amount {Number} The amount to increment by.
         */
        increment: function(attr, amount) {
          if (_.isUndefined(amount) || _.isNull(amount)) {
            amount = 1;
          }
          return this.set(attr, new AV.Op.Increment(amount));
        },
  
        /**
         * Atomically add an object to the end of the array associated with a given
         * key.
         * @param key {String} The key.
         * @param item {} The item to add.
         */
        add: function(attr, item) {
          return this.set(attr, new AV.Op.Add(ensureArray(item)));
        },
  
        /**
         * Atomically add an object to the array associated with a given key, only
         * if it is not already present in the array. The position of the insert is
         * not guaranteed.
         *
         * @param key {String} The key.
         * @param item {} The object to add.
         */
        addUnique: function(attr, item) {
          return this.set(attr, new AV.Op.AddUnique(ensureArray(item)));
        },
  
        /**
         * Atomically remove all instances of an object from the array associated
         * with a given key.
         *
         * @param key {String} The key.
         * @param item {} The object to remove.
         */
        remove: function(attr, item) {
          return this.set(attr, new AV.Op.Remove(ensureArray(item)));
        },
  
        /**
         * Atomically apply a "bit and" operation on the value associated with a
         * given key.
         *
         * @param key {String} The key.
         * @param value {Number} The value to apply.
         */
        bitAnd(attr, value) {
          return this.set(attr, new AV.Op.BitAnd(value));
        },
  
        /**
         * Atomically apply a "bit or" operation on the value associated with a
         * given key.
         *
         * @param key {String} The key.
         * @param value {Number} The value to apply.
         */
        bitOr(attr, value) {
          return this.set(attr, new AV.Op.BitOr(value));
        },
  
        /**
         * Atomically apply a "bit xor" operation on the value associated with a
         * given key.
         *
         * @param key {String} The key.
         * @param value {Number} The value to apply.
         */
        bitXor(attr, value) {
          return this.set(attr, new AV.Op.BitXor(value));
        },
  
        /**
         * Returns an instance of a subclass of AV.Op describing what kind of
         * modification has been performed on this field since the last time it was
         * saved. For example, after calling object.increment("x"), calling
         * object.op("x") would return an instance of AV.Op.Increment.
         *
         * @param key {String} The key.
         * @returns {AV.Op} The operation, or undefined if none.
         */
        op: function(attr) {
          return _.last(this._opSetQueue)[attr];
        },
  
        /**
         * Clear all attributes on the model, firing <code>"change"</code> unless
         * you choose to silence it.
         */
        clear: function(options) {
          options = options || {};
          options.unset = true;
          var keysToClear = _.extend(this.attributes, this._operations);
          return this.set(keysToClear, options);
        },
  
        /**
         * Clears any (or specific) changes to the model made since the last save.
         * @param {string|string[]} [keys] specify keys to revert.
         */
        revert(keys) {
          const lastOp = _.last(this._opSetQueue);
          const _keys = ensureArray(keys || _.keys(lastOp));
          _keys.forEach(key => {
            delete lastOp[key];
          });
          this._rebuildAllEstimatedData();
          return this;
        },
  
        /**
         * Returns a JSON-encoded set of operations to be sent with the next save
         * request.
         * @private
         */
        _getSaveJSON: function() {
          var json = _.clone(_.first(this._opSetQueue));
          AV._objectEach(json, function(op, key) {
            json[key] = op.toJSON();
          });
          return json;
        },
  
        /**
         * Returns true if this object can be serialized for saving.
         * @private
         */
        _canBeSerialized: function() {
          return AV.Object._canBeSerializedAsValue(this.attributes);
        },
  
        /**
         * Fetch the model from the server. If the server's representation of the
         * model differs from its current attributes, they will be overriden,
         * triggering a <code>"change"</code> event.
         * @param {Object} fetchOptions Optional options to set 'keys',
         *      'include' and 'includeACL' option.
         * @param {AuthOptions} options
         * @return {Promise} A promise that is fulfilled when the fetch
         *     completes.
         */
        fetch: function(fetchOptions = {}, options) {
          var self = this;
          var request = _request(
            'classes',
            this.className,
            this.id,
            'GET',
            transformFetchOptions(fetchOptions),
            options
          );
          return request.then(function(response) {
            const fetchedAttrs = self.parse(response);
            self._cleanupUnsetKeys(
              fetchedAttrs,
              fetchOptions.keys
                ? ensureArray(fetchOptions.keys)
                    .join(',')
                    .split(',')
                : undefined
            );
            self._finishFetch(fetchedAttrs, true);
            return self;
          });
        },
  
        _cleanupUnsetKeys(fetchedAttrs, fetchedKeys = _.keys(this._serverData)) {
          _.forEach(fetchedKeys, key => {
            if (fetchedAttrs[key] === undefined) delete this._serverData[key];
          });
        },
  
        /**
         * Set a hash of model attributes, and save the model to the server.
         * updatedAt will be updated when the request returns.
         * You can either call it as:<pre>
         *   object.save();</pre>
         * or<pre>
         *   object.save(null, options);</pre>
         * or<pre>
         *   object.save(attrs, options);</pre>
         * or<pre>
         *   object.save(key, value, options);</pre>
         *
         * @example
         * gameTurn.save({
         *   player: "Jake Cutter",
         *   diceRoll: 2
         * }).then(function(gameTurnAgain) {
         *   // The save was successful.
         * }, function(error) {
         *   // The save failed.  Error is an instance of AVError.
         * });
         *
         * @param {AuthOptions} options AuthOptions plus:
         * @param {Boolean} options.fetchWhenSave fetch and update object after save succeeded
         * @param {AV.Query} options.query Save object only when it matches the query
         * @return {Promise} A promise that is fulfilled when the save
         *     completes.
         * @see AVError
         */
        save: function(arg1, arg2, arg3) {
          var attrs, current, options;
          if (_.isObject(arg1) || isNullOrUndefined(arg1)) {
            attrs = arg1;
            options = arg2;
          } else {
            attrs = {};
            attrs[arg1] = arg2;
            options = arg3;
          }
  
          options = _.clone(options) || {};
          if (options.wait) {
            current = _.clone(this.attributes);
          }
  
          var setOptions = _.clone(options) || {};
          if (setOptions.wait) {
            setOptions.silent = true;
          }
          if (attrs) {
            this.set(attrs, setOptions);
          }
  
          var model = this;
  
          var unsavedChildren = [];
          var unsavedFiles = [];
          AV.Object._findUnsavedChildren(model, unsavedChildren, unsavedFiles);
          if (unsavedChildren.length + unsavedFiles.length > 1) {
            return AV.Object._deepSaveAsync(this, model, options);
          }
  
          this._startSave();
          this._saving = (this._saving || 0) + 1;
  
          this._allPreviousSaves = this._allPreviousSaves || Promise.resolve();
          this._allPreviousSaves = this._allPreviousSaves
            .catch(e => {})
            .then(function() {
              var method = model.id ? 'PUT' : 'POST';
  
              var json = model._getSaveJSON();
              var query = {};
  
              if (model._fetchWhenSave || options.fetchWhenSave) {
                query['new'] = 'true';
              }
              // user login option
              if (options._failOnNotExist) {
                query.failOnNotExist = 'true';
              }
  
              if (options.query) {
                var queryParams;
                if (typeof options.query._getParams === 'function') {
                  queryParams = options.query._getParams();
                  if (queryParams) {
                    query.where = queryParams.where;
                  }
                }
                if (!query.where) {
                  var error = new Error('options.query is not an AV.Query');
                  throw error;
                }
              }
  
              _.extend(json, model._flags);
  
              var route = 'classes';
              var className = model.className;
              if (model.className === '_User' && !model.id) {
                // Special-case user sign-up.
                route = 'users';
                className = null;
              }
              //hook makeRequest in options.
              var makeRequest = options._makeRequest || _request;
              var requestPromise = makeRequest(
                route,
                className,
                model.id,
                method,
                json,
                options,
                query
              );
  
              requestPromise = requestPromise.then(
                function(resp) {
                  var serverAttrs = model.parse(resp);
                  if (options.wait) {
                    serverAttrs = _.extend(attrs || {}, serverAttrs);
                  }
                  model._finishSave(serverAttrs);
                  if (options.wait) {
                    model.set(current, setOptions);
                  }
                  return model;
                },
                function(error) {
                  model._cancelSave();
                  throw error;
                }
              );
  
              return requestPromise;
            });
          return this._allPreviousSaves;
        },
  
        /**
         * Destroy this model on the server if it was already persisted.
         * Optimistically removes the model from its collection, if it has one.
         * @param {AuthOptions} options AuthOptions plus:
         * @param {Boolean} [options.wait] wait for the server to respond
         * before removal.
         *
         * @return {Promise} A promise that is fulfilled when the destroy
         *     completes.
         */
        destroy: function(options) {
          options = options || {};
          var model = this;
  
          var triggerDestroy = function() {
            model.trigger('destroy', model, model.collection, options);
          };
  
          if (!this.id) {
            return triggerDestroy();
          }
  
          if (!options.wait) {
            triggerDestroy();
          }
  
          var request = _request(
            'classes',
            this.className,
            this.id,
            'DELETE',
            this._flags,
            options
          );
          return request.then(function() {
            if (options.wait) {
              triggerDestroy();
            }
            return model;
          });
        },
  
        /**
         * Converts a response into the hash of attributes to be set on the model.
         * @ignore
         */
        parse: function(resp) {
          var output = _.clone(resp);
          _(['createdAt', 'updatedAt']).each(function(key) {
            if (output[key]) {
              output[key] = AV._parseDate(output[key]);
            }
          });
          if (output.createdAt && !output.updatedAt) {
            output.updatedAt = output.createdAt;
          }
          return output;
        },
  
        /**
         * Creates a new model with identical attributes to this one.
         * @return {AV.Object}
         */
        clone: function() {
          return new this.constructor(this.attributes);
        },
  
        /**
         * Returns true if this object has never been saved to AV.
         * @return {Boolean}
         */
        isNew: function() {
          return !this.id;
        },
  
        /**
         * Call this method to manually fire a `"change"` event for this model and
         * a `"change:attribute"` event for each changed attribute.
         * Calling this will cause all objects observing the model to update.
         */
        change: function(options) {
          options = options || {};
          var changing = this._changing;
          this._changing = true;
  
          // Silent changes become pending changes.
          var self = this;
          AV._objectEach(this._silent, function(attr) {
            self._pending[attr] = true;
          });
  
          // Silent changes are triggered.
          var changes = _.extend({}, options.changes, this._silent);
          this._silent = {};
          AV._objectEach(changes, function(unused_value, attr) {
            self.trigger('change:' + attr, self, self.get(attr), options);
          });
          if (changing) {
            return this;
          }
  
          // This is to get around lint not letting us make a function in a loop.
          var deleteChanged = function(value, attr) {
            if (!self._pending[attr] && !self._silent[attr]) {
              delete self.changed[attr];
            }
          };
  
          // Continue firing `"change"` events while there are pending changes.
          while (!_.isEmpty(this._pending)) {
            this._pending = {};
            this.trigger('change', this, options);
            // Pending and silent changes still remain.
            AV._objectEach(this.changed, deleteChanged);
            self._previousAttributes = _.clone(this.attributes);
          }
  
          this._changing = false;
          return this;
        },
  
        /**
         * Determine if the model has changed since the last <code>"change"</code>
         * event.  If you specify an attribute name, determine if that attribute
         * has changed.
         * @param {String} attr Optional attribute name
         * @return {Boolean}
         */
        hasChanged: function(attr) {
          if (!arguments.length) {
            return !_.isEmpty(this.changed);
          }
          return this.changed && _.has(this.changed, attr);
        },
  
        /**
         * Returns an object containing all the attributes that have changed, or
         * false if there are no changed attributes. Useful for determining what
         * parts of a view need to be updated and/or what attributes need to be
         * persisted to the server. Unset attributes will be set to undefined.
         * You can also pass an attributes object to diff against the model,
         * determining if there *would be* a change.
         */
        changedAttributes: function(diff) {
          if (!diff) {
            return this.hasChanged() ? _.clone(this.changed) : false;
          }
          var changed = {};
          var old = this._previousAttributes;
          AV._objectEach(diff, function(diffVal, attr) {
            if (!_.isEqual(old[attr], diffVal)) {
              changed[attr] = diffVal;
            }
          });
          return changed;
        },
  
        /**
         * Gets the previous value of an attribute, recorded at the time the last
         * <code>"change"</code> event was fired.
         * @param {String} attr Name of the attribute to get.
         */
        previous: function(attr) {
          if (!arguments.length || !this._previousAttributes) {
            return null;
          }
          return this._previousAttributes[attr];
        },
  
        /**
         * Gets all of the attributes of the model at the time of the previous
         * <code>"change"</code> event.
         * @return {Object}
         */
        previousAttributes: function() {
          return _.clone(this._previousAttributes);
        },
  
        /**
         * Checks if the model is currently in a valid state. It's only possible to
         * get into an *invalid* state if you're using silent changes.
         * @return {Boolean}
         */
        isValid: function() {
          try {
            this.validate(this.attributes);
          } catch (error) {
            return false;
          }
          return true;
        },
  
        /**
         * You should not call this function directly unless you subclass
         * <code>AV.Object</code>, in which case you can override this method
         * to provide additional validation on <code>set</code> and
         * <code>save</code>.  Your implementation should throw an Error if
         * the attrs is invalid
         *
         * @param {Object} attrs The current data to validate.
         * @see AV.Object#set
         */
        validate: function(attrs) {
          if (_.has(attrs, 'ACL') && !(attrs.ACL instanceof AV.ACL)) {
            throw new AVError(AVError.OTHER_CAUSE, 'ACL must be a AV.ACL.');
          }
        },
  
        /**
         * Run validation against a set of incoming attributes, returning `true`
         * if all is well. If a specific `error` callback has been passed,
         * call that instead of firing the general `"error"` event.
         * @private
         */
        _validate: function(attrs, options) {
          if (options.silent || !this.validate) {
            return;
          }
          attrs = _.extend({}, this.attributes, attrs);
          this.validate(attrs);
        },
  
        /**
         * Returns the ACL for this object.
         * @returns {AV.ACL} An instance of AV.ACL.
         * @see AV.Object#get
         */
        getACL: function() {
          return this.get('ACL');
        },
  
        /**
         * Sets the ACL to be used for this object.
         * @param {AV.ACL} acl An instance of AV.ACL.
         * @param {Object} options Optional Backbone-like options object to be
         *     passed in to set.
         * @return {AV.Object} self
         * @see AV.Object#set
         */
        setACL: function(acl, options) {
          return this.set('ACL', acl, options);
        },
  
        disableBeforeHook: function() {
          this.ignoreHook('beforeSave');
          this.ignoreHook('beforeUpdate');
          this.ignoreHook('beforeDelete');
        },
  
        disableAfterHook: function() {
          this.ignoreHook('afterSave');
          this.ignoreHook('afterUpdate');
          this.ignoreHook('afterDelete');
        },
  
        ignoreHook: function(hookName) {
          if (
            !_.contains(
              [
                'beforeSave',
                'afterSave',
                'beforeUpdate',
                'afterUpdate',
                'beforeDelete',
                'afterDelete',
              ],
              hookName
            )
          ) {
            console.trace('Unsupported hookName: ' + hookName);
          }
  
          if (!AV.hookKey) {
            console.trace('ignoreHook required hookKey');
          }
  
          if (!this._flags.__ignore_hooks) {
            this._flags.__ignore_hooks = [];
          }
  
          this._flags.__ignore_hooks.push(hookName);
        },
      }
    );
  
    /**
     * Creates an instance of a subclass of AV.Object for the give classname
     * and id.
     * @param  {String|Function} class the className or a subclass of AV.Object.
     * @param {String} id The object id of this model.
     * @return {AV.Object} A new subclass instance of AV.Object.
     */
    AV.Object.createWithoutData = (klass, id, hasData) => {
      let _klass;
      if (_.isString(klass)) {
        _klass = AV.Object._getSubclass(klass);
      } else if (klass.prototype && klass.prototype instanceof AV.Object) {
        _klass = klass;
      } else {
        throw new Error('class must be a string or a subclass of AV.Object.');
      }
      const object = new _klass();
      object.id = id;
      object._hasData = hasData;
      return object;
    };
    /**
     * Delete objects in batch.
     * @param {AV.Object[]} objects The <code>AV.Object</code> array to be deleted.
     * @param {AuthOptions} options
     * @return {Promise} A promise that is fulfilled when the save
     *     completes.
     */
    AV.Object.destroyAll = function(objects, options = {}) {
      if (!objects || objects.length === 0) {
        return Promise.resolve();
      }
      const objectsByClassNameAndFlags = _.groupBy(objects, object =>
        JSON.stringify({
          className: object.className,
          flags: object._flags,
        })
      );
      const body = {
        requests: _.map(objectsByClassNameAndFlags, objects => {
          const ids = _.map(objects, 'id').join(',');
          return {
            method: 'DELETE',
            path: `/1.1/classes/${objects[0].className}/${ids}`,
            body: objects[0]._flags,
          };
        }),
      };
      return _request('batch', null, null, 'POST', body, options).then(
        response => {
          const firstError = _.find(response, result => !result.success);
          if (firstError)
            throw new AVError(firstError.error.code, firstError.error.error);
          return undefined;
        }
      );
    };
  
    /**
     * Returns the appropriate subclass for making new instances of the given
     * className string.
     * @private
     */
    AV.Object._getSubclass = function(className) {
      if (!_.isString(className)) {
        throw new Error('AV.Object._getSubclass requires a string argument.');
      }
      var ObjectClass = AV.Object._classMap[className];
      if (!ObjectClass) {
        ObjectClass = AV.Object.extend(className);
        AV.Object._classMap[className] = ObjectClass;
      }
      return ObjectClass;
    };
  
    /**
     * Creates an instance of a subclass of AV.Object for the given classname.
     * @private
     */
    AV.Object._create = function(className, attributes, options) {
      var ObjectClass = AV.Object._getSubclass(className);
      return new ObjectClass(attributes, options);
    };
  
    // Set up a map of className to class so that we can create new instances of
    // AV Objects from JSON automatically.
    AV.Object._classMap = {};
  
    AV.Object._extend = AV._extend;
  
    /**
     * Creates a new model with defined attributes,
     * It's the same with
     * <pre>
     *   new AV.Object(attributes, options);
     *  </pre>
     * @param {Object} attributes The initial set of data to store in the object.
     * @param {Object} options A set of Backbone-like options for creating the
     *     object.  The only option currently supported is "collection".
     * @return {AV.Object}
     * @since v0.4.4
     * @see AV.Object
     * @see AV.Object.extend
     */
    AV.Object['new'] = function(attributes, options) {
      return new AV.Object(attributes, options);
    };
  
    /**
     * Creates a new subclass of AV.Object for the given AV class name.
     *
     * <p>Every extension of a AV class will inherit from the most recent
     * previous extension of that class. When a AV.Object is automatically
     * created by parsing JSON, it will use the most recent extension of that
     * class.</p>
     *
     * @example
     * var MyClass = AV.Object.extend("MyClass", {
     *     // Instance properties
     * }, {
     *     // Class properties
     * });
     *
     * @param {String} className The name of the AV class backing this model.
     * @param {Object} protoProps Instance properties to add to instances of the
     *     class returned from this method.
     * @param {Object} classProps Class properties to add the class returned from
     *     this method.
     * @return {Class} A new subclass of AV.Object.
     */
    AV.Object.extend = function(className, protoProps, classProps) {
      // Handle the case with only two args.
      if (!_.isString(className)) {
        if (className && _.has(className, 'className')) {
          return AV.Object.extend(className.className, className, protoProps);
        } else {
          throw new Error(
            "AV.Object.extend's first argument should be the className."
          );
        }
      }
  
      // If someone tries to subclass "User", coerce it to the right type.
      if (className === 'User') {
        className = '_User';
      }
  
      var NewClassObject = null;
      if (_.has(AV.Object._classMap, className)) {
        var OldClassObject = AV.Object._classMap[className];
        // This new subclass has been told to extend both from "this" and from
        // OldClassObject. This is multiple inheritance, which isn't supported.
        // For now, let's just pick one.
        if (protoProps || classProps) {
          NewClassObject = OldClassObject._extend(protoProps, classProps);
        } else {
          return OldClassObject;
        }
      } else {
        protoProps = protoProps || {};
        protoProps._className = className;
        NewClassObject = this._extend(protoProps, classProps);
      }
      // Extending a subclass should reuse the classname automatically.
      NewClassObject.extend = function(arg0) {
        if (_.isString(arg0) || (arg0 && _.has(arg0, 'className'))) {
          return AV.Object.extend.apply(NewClassObject, arguments);
        }
        var newArguments = [className].concat(_.toArray(arguments));
        return AV.Object.extend.apply(NewClassObject, newArguments);
      };
      // Add the query property descriptor.
      Object.defineProperty(
        NewClassObject,
        'query',
        Object.getOwnPropertyDescriptor(AV.Object, 'query')
      );
      NewClassObject['new'] = function(attributes, options) {
        return new NewClassObject(attributes, options);
      };
      AV.Object._classMap[className] = NewClassObject;
      return NewClassObject;
    };
  
    // ES6 class syntax support
    Object.defineProperty(AV.Object.prototype, 'className', {
      get: function() {
        const className =
          this._className ||
          this.constructor._LCClassName ||
          this.constructor.name;
        // If someone tries to subclass "User", coerce it to the right type.
        if (className === 'User') {
          return '_User';
        }
        return className;
      },
    });
  
    /**
     * Register a class.
     * If a subclass of <code>AV.Object</code> is defined with your own implement
     * rather then <code>AV.Object.extend</code>, the subclass must be registered.
     * @param {Function} klass A subclass of <code>AV.Object</code>
     * @param {String} [name] Specify the name of the class. Useful when the class might be uglified.
     * @example
     * class Person extend AV.Object {}
     * AV.Object.register(Person);
     */
    AV.Object.register = (klass, name) => {
      if (!(klass.prototype instanceof AV.Object)) {
        throw new Error('registered class is not a subclass of AV.Object');
      }
      const className = name || klass.name;
      if (!className.length) {
        throw new Error('registered class must be named');
      }
      if (name) {
        klass._LCClassName = name;
      }
      AV.Object._classMap[className] = klass;
    };
  
    /**
     * Get a new Query of the current class
     * @name query
     * @memberof AV.Object
     * @type AV.Query
     * @readonly
     * @since v3.1.0
     * @example
     * const Post = AV.Object.extend('Post');
     * Post.query.equalTo('author', 'leancloud').find().then();
     */
    Object.defineProperty(AV.Object, 'query', {
      get() {
        return new AV.Query(this.prototype.className);
      },
    });
  
    AV.Object._findUnsavedChildren = function(objects, children, files) {
      AV._traverse(objects, function(object) {
        if (object instanceof AV.Object) {
          if (object.dirty()) {
            children.push(object);
          }
          return;
        }
  
        if (object instanceof AV.File) {
          if (!object.id) {
            files.push(object);
          }
          return;
        }
      });
    };
  
    AV.Object._canBeSerializedAsValue = function(object) {
      var canBeSerializedAsValue = true;
  
      if (object instanceof AV.Object || object instanceof AV.File) {
        canBeSerializedAsValue = !!object.id;
      } else if (_.isArray(object)) {
        AV._arrayEach(object, function(child) {
          if (!AV.Object._canBeSerializedAsValue(child)) {
            canBeSerializedAsValue = false;
          }
        });
      } else if (_.isObject(object)) {
        AV._objectEach(object, function(child) {
          if (!AV.Object._canBeSerializedAsValue(child)) {
            canBeSerializedAsValue = false;
          }
        });
      }
  
      return canBeSerializedAsValue;
    };
  
    AV.Object._deepSaveAsync = function(object, model, options) {
      var unsavedChildren = [];
      var unsavedFiles = [];
      AV.Object._findUnsavedChildren(object, unsavedChildren, unsavedFiles);
  
      var promise = Promise.resolve();
      _.each(unsavedFiles, function(file) {
        promise = promise.then(function() {
          return file.save();
        });
      });
  
      var objects = _.uniq(unsavedChildren);
      var remaining = _.uniq(objects);
  
      return promise
        .then(function() {
          return Promise._continueWhile(
            function() {
              return remaining.length > 0;
            },
            function() {
              // Gather up all the objects that can be saved in this batch.
              var batch = [];
              var newRemaining = [];
              AV._arrayEach(remaining, function(object) {
                // Limit batches to 20 objects.
                if (batch.length > 20) {
                  newRemaining.push(object);
                  return;
                }
  
                if (object._canBeSerialized()) {
                  batch.push(object);
                } else {
                  newRemaining.push(object);
                }
              });
              remaining = newRemaining;
  
              // If we can't save any objects, there must be a circular reference.
              if (batch.length === 0) {
                return Promise.reject(
                  new AVError(
                    AVError.OTHER_CAUSE,
                    'Tried to save a batch with a cycle.'
                  )
                );
              }
  
              // Reserve a spot in every object's save queue.
              var readyToStart = Promise.resolve(
                _.map(batch, function(object) {
                  return object._allPreviousSaves || Promise.resolve();
                })
              );
  
              // Save a single batch, whether previous saves succeeded or failed.
              const bathSavePromise = readyToStart.then(() =>
                _request(
                  'batch',
                  null,
                  null,
                  'POST',
                  {
                    requests: _.map(batch, function(object) {
                      var method = object.id ? 'PUT' : 'POST';
  
                      var json = object._getSaveJSON();
  
                      _.extend(json, object._flags);
  
                      var route = 'classes';
                      var className = object.className;
                      var path = `/${route}/${className}`;
                      if (object.className === '_User' && !object.id) {
                        // Special-case user sign-up.
                        path = '/users';
                      }
  
                      var path = `/1.1${path}`;
                      if (object.id) {
                        path = path + '/' + object.id;
                      }
  
                      object._startSave();
  
                      return {
                        method: method,
                        path: path,
                        body: json,
                      };
                    }),
                  },
                  options
                ).then(function(response) {
                  const results = _.map(batch, function(object, i) {
                    if (response[i].success) {
                      object._finishSave(object.parse(response[i].success));
                      return object;
                    }
                    object._cancelSave();
                    return new AVError(
                      response[i].error.code,
                      response[i].error.error
                    );
                  });
                  return handleBatchResults(results);
                })
              );
              AV._arrayEach(batch, function(object) {
                object._allPreviousSaves = bathSavePromise;
              });
              return bathSavePromise;
            }
          );
        })
        .then(function() {
          return object;
        });
    };
  };
  