# av.js

    const _ = require('underscore');
    const userAgent = require('./ua');
    const {
      isNullOrUndefined,
    } = require('./utils');

    const AV = global.AV || {};

    // All internal configuration items
    AV._config = AV._config || {};
    const AVConfig = AV._config;

    _.extend(AVConfig, {

      // 服务器节点地区，默认中国大陆
      region: 'cn',

      // 服务器的 URL，默认初始化时被设置为大陆节点地址
      APIServerURL: AVConfig.APIServerURL || '',

      // 禁用 currentUser，通常用于多用户环境
      disableCurrentUser: false,

      // Internal config can modifie the UserAgent
      userAgent,

      // set production environment or test environment
      // 1: production environment, 0: test environment, null: default environment
      applicationProduction: null,
    });

    /**
     * Contains all AV API classes and functions.
     * @namespace AV
     */

    // Helpers
    // -------

    // Shared empty constructor function to aid in prototype-chain creation.
    var EmptyConstructor = function() {};

    // Helper function to correctly set up the prototype chain, for subclasses.
    // Similar to `goog.inherits`, but uses a hash of prototype properties and
    // class properties to be extended.
    var inherits = function(parent, protoProps, staticProps) {
      var child;

      // The constructor function for the new subclass is either defined by you
      // (the "constructor" property in your `extend` definition), or defaulted
      // by us to simply call the parent's constructor.
      if (protoProps && protoProps.hasOwnProperty('constructor')) {
        child = protoProps.constructor;
      } else {
        /** @ignore */
        child = function(){ parent.apply(this, arguments); };
      }

      // Inherit class (static) properties from parent.
      _.extend(child, parent);

      // Set the prototype chain to inherit from `parent`, without calling
      // `parent`'s constructor function.
      EmptyConstructor.prototype = parent.prototype;
      child.prototype = new EmptyConstructor();

      // Add prototype properties (instance properties) to the subclass,
      // if supplied.
      if (protoProps) {
        _.extend(child.prototype, protoProps);
      }

      // Add static properties to the constructor function, if supplied.
      if (staticProps) {
        _.extend(child, staticProps);
      }

      // Correctly set child's `prototype.constructor`.
      child.prototype.constructor = child;

      // Set a convenience property in case the parent's prototype is
      // needed later.
      child.__super__ = parent.prototype;

      return child;
    };

    /**
     * Call this method to set production environment variable.
     * @function AV.setProduction
     * @param {Boolean} production True is production environment,and
     *  it's true by default.
     */
    AV.setProduction = (production) => {
      if (!isNullOrUndefined(production)) {
        AVConfig.applicationProduction = production ? 1 : 0;
      } else {
        // change to default value
        AVConfig.applicationProduction = null;
      }
    };

    /**
     * Returns prefix for localStorage keys used by this instance of AV.
     * @param {String} path The relative suffix to append to it.
     *     null or undefined is treated as the empty string.
     * @return {String} The full key name.
     * @private
     */
    AV._getAVPath = function(path) {
      if (!AV.applicationId) {
        throw new Error("You need to call AV.initialize before using AV.");
      }
      if (!path) {
        path = "";
      }
      if (!_.isString(path)) {
        throw new Error("Tried to get a localStorage path that wasn't a String.");
      }
      if (path[0] === "/") {
        path = path.substring(1);
      }
      return "AV/" + AV.applicationId + "/" + path;
    };

    /**
     * Returns the unique string for this app on this machine.
     * Gets reset when localStorage is cleared.
     * @private
     */
    AV._installationId = null;
    AV._getInstallationId = function() {
      // See if it's cached in RAM.
      if (AV._installationId) {
        return AV.Promise.resolve(AV._installationId);
      }

      // Try to get it from localStorage.
      var path = AV._getAVPath("installationId");
      return AV.localStorage.getItemAsync(path).then(function(_installationId){
        AV._installationId = _installationId;
        if (!AV._installationId) {
          // It wasn't in localStorage, so create a new one.
          var hexOctet = function() {
            return Math.floor((1+Math.random())*0x10000).toString(16).substring(1);
          };
          AV._installationId = (
            hexOctet() + hexOctet() + "-" +
            hexOctet() + "-" +
            hexOctet() + "-" +
            hexOctet() + "-" +
            hexOctet() + hexOctet() + hexOctet());
          return AV.localStorage.setItemAsync(path, AV._installationId);
        }
        else {
          return _installationId;
        }
      });
    };

    AV._parseDate = function(iso8601) {
      var regexp = new RegExp(
        "^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})" + "T" +
        "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})" +
        "(.([0-9]+))?" + "Z$");
      var match = regexp.exec(iso8601);
      if (!match) {
        return null;
      }

      var year = match[1] || 0;
      var month = (match[2] || 1) - 1;
      var day = match[3] || 0;
      var hour = match[4] || 0;
      var minute = match[5] || 0;
      var second = match[6] || 0;
      var milli = match[8] || 0;

      return new Date(Date.UTC(year, month, day, hour, minute, second, milli));
    };

    // A self-propagating extend function.
    AV._extend = function(protoProps, classProps) {
      var child = inherits(this, protoProps, classProps);
      child.extend = this.extend;
      return child;
    };

    // Helper function to get a value from a Backbone object as a property
    // or as a function.
    AV._getValue = function(object, prop) {
      if (!(object && object[prop])) {
        return null;
      }
      return _.isFunction(object[prop]) ? object[prop]() : object[prop];
    };

    /**
     * Converts a value in a AV Object into the appropriate representation.
     * This is the JS equivalent of Java's AV.maybeReferenceAndEncode(Object)
     * if seenObjects is falsey. Otherwise any AV.Objects not in
     * seenObjects will be fully embedded rather than encoded
     * as a pointer.  This array will be used to prevent going into an infinite
     * loop because we have circular references.  If <seenObjects>
     * is set, then none of the AV Objects that are serialized can be dirty.
     * @private
     */
    AV._encode = function(value, seenObjects, disallowObjects) {
      if (value instanceof AV.Object) {
        if (disallowObjects) {
          throw new Error("AV.Objects not allowed here");
        }
        if (!seenObjects || _.include(seenObjects, value) || !value._hasData) {
          return value._toPointer();
        }
        if (!value.dirty()) {
          seenObjects = seenObjects.concat(value);
          return AV._encode(value._toFullJSON(seenObjects),
                               seenObjects,
                               disallowObjects);
        }
        throw new Error("Tried to save an object with a pointer to a new, unsaved object.");
      }
      if (value instanceof AV.ACL) {
        return value.toJSON();
      }
      if (_.isDate(value)) {
        return { "__type": "Date", "iso": value.toJSON() };
      }
      if (value instanceof AV.GeoPoint) {
        return value.toJSON();
      }
      if (_.isArray(value)) {
        return _.map(value, function(x) {
          return AV._encode(x, seenObjects, disallowObjects);
        });
      }
      if (_.isRegExp(value)) {
        return value.source;
      }
      if (value instanceof AV.Relation) {
        return value.toJSON();
      }
      if (value instanceof AV.Op) {
        return value.toJSON();
      }
      if (value instanceof AV.File) {
        if (!value.url() && !value.id) {
          throw new Error("Tried to save an object containing an unsaved file.");
        }
        return {
          __type: "File",
          id:  value.id,
          name: value.name(),
          url: value.url()
        };
      }
      if (_.isObject(value)) {
        return _.mapObject(value, (v, k) => AV._encode(v, seenObjects, disallowObjects));
      }
      return value;
    };

    /**
     * The inverse function of AV._encode.
     * @private
     */
    AV._decode = function(value, key) {
      if (!_.isObject(value) || _.isDate(value)) {
        return value;
      }
      if (_.isArray(value)) {
        return _.map(value, v => AV._decode(v));
      }
      if (value instanceof AV.Object) {
        return value;
      }
      if (value instanceof AV.File) {
        return value;
      }
      if (value instanceof AV.Op) {
        return value;
      }
      if (value instanceof AV.GeoPoint) {
        return value;
      }
      if (value instanceof AV.ACL) {
        return value;
      }
      if (value.__op) {
        return AV.Op._decode(value);
      }
      var className;
      if (value.__type === "Pointer") {
        className = value.className;
        var pointer = AV.Object._create(className);
        if(Object.keys(value).length > 3) {
            const v = _.clone(value);
            delete v.__type;
            delete v.className;
            pointer._finishFetch(v, true);
        }else{
            pointer._finishFetch({ objectId: value.objectId }, false);
        }
        return pointer;
      }
      if (value.__type === "Object") {
        // It's an Object included in a query result.
        className = value.className;
        const v = _.clone(value);
        delete v.__type;
        delete v.className;
        var object = AV.Object._create(className);
        object._finishFetch(v, true);
        return object;
      }
      if (value.__type === "Date") {
        return AV._parseDate(value.iso);
      }
      if (value.__type === "GeoPoint") {
        return new AV.GeoPoint({
          latitude: value.latitude,
          longitude: value.longitude
        });
      }
      if (value.__type === "Relation") {
        if (!key) throw new Error('key missing decoding a Relation');
        var relation = new AV.Relation(null, key);
        relation.targetClassName = value.className;
        return relation;
      }
      if (value.__type === 'File') {
        var file = new AV.File(value.name);
        const v = _.clone(value);
        delete v.__type;
        file._finishFetch(v);
        return file;
      }
      return _.mapObject(value, function(v, k) {
        if (k === "ACL") {
          return new AV.ACL(v);
        }
        return AV._decode(v, k);
      });
    };

    AV._encodeObjectOrArray = function(value) {
      var encodeAVObject = function(object) {
        if (object && object._toFullJSON){
          object = object._toFullJSON([]);
        }

        return _.mapObject(object, function(value) {
          return AV._encode(value, []);
        });
      };

      if (_.isArray(value)) {
        return value.map(function(object) {
          return encodeAVObject(object);
        });
      } else {
        return encodeAVObject(value);
      }
    };

    AV._arrayEach = _.each;

    /**
     * Does a deep traversal of every item in object, calling func on every one.
     * @param {Object} object The object or array to traverse deeply.
     * @param {Function} func The function to call for every item. It will
     *     be passed the item as an argument. If it returns a truthy value, that
     *     value will replace the item in its parent container.
     * @returns {} the result of calling func on the top-level object itself.
     * @private
     */
    AV._traverse = function(object, func, seen) {
      if (object instanceof AV.Object) {
        seen = seen || [];
        if (_.indexOf(seen, object) >= 0) {
          // We've already visited this object in this call.
          return;
        }
        seen.push(object);
        AV._traverse(object.attributes, func, seen);
        return func(object);
      }
      if (object instanceof AV.Relation || object instanceof AV.File) {
        // Nothing needs to be done, but we don't want to recurse into the
        // object's parent infinitely, so we catch this case.
        return func(object);
      }
      if (_.isArray(object)) {
        _.each(object, function(child, index) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[index] = newChild;
          }
        });
        return func(object);
      }
      if (_.isObject(object)) {
        AV._each(object, function(child, key) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[key] = newChild;
          }
        });
        return func(object);
      }
      return func(object);
    };

    /**
     * This is like _.each, except:
     * * it doesn't work for so-called array-like objects,
     * * it does work for dictionaries with a "length" attribute.
     * @private
     */
    AV._objectEach = AV._each = function(obj, callback) {
      if (_.isObject(obj)) {
        _.each(_.keys(obj), function(key) {
          callback(obj[key], key);
        });
      } else {
        _.each(obj, callback);
      }
    };

    module.exports = AV;